Skip to content

Commit

Permalink
Merge pull request #471 from LF-Decentralized-Trust-labs/abi-upload
Browse files Browse the repository at this point in the history
ABI Upload
  • Loading branch information
dwertent authored Dec 17, 2024
2 parents a4ea70e + 52417e4 commit 7f9adcd
Show file tree
Hide file tree
Showing 14 changed files with 709 additions and 71 deletions.
259 changes: 253 additions & 6 deletions ui/client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ui/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"lucide-react": "^0.454.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-drag-drop-files": "^2.4.0",
"react-i18next": "^15.0.2",
"react-infinite-scroll-component": "^6.1.0",
"react-json-pretty": "^2.2.0",
Expand Down
9 changes: 5 additions & 4 deletions ui/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ 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";

const queryClient = new QueryClient({
queryCache: new QueryCache({}),
Expand Down Expand Up @@ -94,10 +95,10 @@ function App() {
<BrowserRouter>
<Header />
<Routes>
<Route path="/ui/indexer" element={<Indexer />} />
<Route path="/ui/submissions" element={<Submissions />} />\
<Route path="/ui/registry" element={<Registries />} />
<Route path="*" element={<Navigate to="/ui/indexer" replace />} />
<Route path={AppRoutes.Indexer} element={<Indexer />} />
<Route path={AppRoutes.Submissions} element={<Submissions />} />\
<Route path={AppRoutes.Registry} element={<Registries />} />
<Route path="*" element={<Navigate to={AppRoutes.Indexer} replace />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
Expand Down
74 changes: 25 additions & 49 deletions ui/client/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { AppBar, Box, Button, Grid2, IconButton, Tab, Tabs, ToggleButton, ToggleButtonGroup, Toolbar, Tooltip, useMediaQuery, useTheme } from "@mui/material";
import { AppBar, Box, Button, Grid2, IconButton, Tab, Tabs, Toolbar, useMediaQuery, useTheme } from "@mui/material";
import { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
import Brightness4Icon from '@mui/icons-material/Brightness4';
import { ApplicationContext } from "../contexts/ApplicationContext";
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause';
import RefreshIcon from '@mui/icons-material/Refresh';
import { SettingsMenu } from "../menus/Settings";
import MenuIcon from '@mui/icons-material/Menu';
import { AppRoutes } from "../routes";

export const Header: React.FC = () => {

const { colorMode, autoRefreshEnabled, setAutoRefreshEnabled, refreshRequired, refresh } = useContext(ApplicationContext);
const { refreshRequired, refresh } = useContext(ApplicationContext);
const { t } = useTranslation();
const navigate = useNavigate();
const pathname = useLocation().pathname.toLowerCase();
const theme = useTheme();
const lessThanMedium = useMediaQuery(theme.breakpoints.down("md"));
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

const getTabFromPath = (path: string) => {
if (path.startsWith('/ui/indexer')) {
if (path.startsWith(AppRoutes.Indexer)) {
return 0;
} else if (path.startsWith('/ui/submissions')) {
} else if (path.startsWith(AppRoutes.Submissions)) {
return 1;
} else if (path.startsWith('/ui/registry')) {
} else if (path.startsWith(AppRoutes.Registry)) {
return 2;
}
return 0;
Expand All @@ -49,16 +50,9 @@ export const Header: React.FC = () => {
const handleNavigation = (tab: number) => {
setTab(tab);
switch (tab) {
case 0: navigate('/ui/indexer'); break;
case 1: navigate('/ui/submissions'); break;
case 2: navigate('/ui/registry'); break;
}
};

const handleAutoRefreshChange = (value: 'play' | 'pause') => {
switch (value) {
case 'play': setAutoRefreshEnabled(true); break;
case 'pause': setAutoRefreshEnabled(false); break;
case 0: navigate(AppRoutes.Indexer); break;
case 1: navigate(AppRoutes.Submissions); break;
case 2: navigate(AppRoutes.Registry); break;
}
};

Expand Down Expand Up @@ -86,38 +80,16 @@ export const Header: React.FC = () => {
<Grid2 container justifyContent={lessThanMedium ? 'center' : 'right'} spacing={1} alignItems="center"
sx={{ padding: lessThanMedium ? '20px' : undefined }}>
{refreshRequired &&
<Grid2>
<Button size="small" startIcon={<RefreshIcon />} variant="outlined" sx={{ textTransform: 'none', borderRadius: '20px' }}
onClick={() => refresh()}>
{t('newData')}
</Button>
</Grid2>}
<Grid2>
<Button size="small" startIcon={<RefreshIcon />} variant="outlined" sx={{ textTransform: 'none', borderRadius: '20px'}}
onClick={() => refresh()}>
{t('newData')}
</Button>
</Grid2>}
<Grid2>
<ToggleButtonGroup exclusive onChange={(_event, value) => handleAutoRefreshChange(value)} value={autoRefreshEnabled ? 'play' : 'pause'}>
<Tooltip arrow title={t('autoRefreshOn')}
slotProps={{ popper: { modifiers: [{ name: 'offset', options: { offset: [0, -6] }, }] } }}
>
<ToggleButton color="primary" value="play">
<PlayArrowIcon fontSize="small" />
</ToggleButton>
</Tooltip>
<Tooltip arrow title={t('autoRefreshOff')}
slotProps={{ popper: { modifiers: [{ name: 'offset', options: { offset: [0, -6] }, }] } }}
>
<ToggleButton color="primary" value="pause">
<PauseIcon fontSize="small" />
</ToggleButton>
</Tooltip>
</ToggleButtonGroup>
</Grid2>
<Grid2>
<Tooltip arrow title={t('switchThemeMode')}
slotProps={{ popper: { modifiers: [{ name: 'offset', options: { offset: [0, -4] }, }] } }}
>
<IconButton onClick={() => colorMode.toggleColorMode()}>
<Brightness4Icon />
</IconButton>
</Tooltip>
<IconButton onClick={event => setAnchorEl(event.currentTarget)}>
<MenuIcon />
</IconButton>
</Grid2>
</Grid2>
</Grid2>
Expand All @@ -129,6 +101,10 @@ export const Header: React.FC = () => {
height: theme => lessThanMedium ? '190px' :
theme.mixins.toolbar
}} />
<SettingsMenu
anchorEl={anchorEl}
setAnchorEl={setAnchorEl}
/>
</>
);

Expand Down
13 changes: 3 additions & 10 deletions ui/client/src/components/PaladinTransaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { EllapsedTime } from "./EllapsedTime";
import VisibilityIcon from '@mui/icons-material/VisibilityOutlined';
import { PaladinTransactionsDetailsDialog } from "../dialogs/TransactionDetails";
import { Captions, Tag } from 'lucide-react';
import { formatJSONWhenApplicable } from "../utils";

daysjs.extend(relativeTime);

Expand All @@ -44,14 +45,6 @@ export const PaladinTransaction: React.FC<Props> = ({ paladinTransaction }) => {
return <></>;
}

const formatProperty = (value: any) => {
try {
const parsed = JSON.stringify(value);
return parsed.substring(1, parsed.length - 1);
} catch (err) { }
return value;
};

return (
<>
<Box
Expand Down Expand Up @@ -114,14 +107,14 @@ export const PaladinTransaction: React.FC<Props> = ({ paladinTransaction }) => {
.map((property) => (
<TextField
key={property}
label={property}
label={t(property)}
maxRows={8}
multiline
fullWidth
size="small"
sx={{ marginTop: '12px' }}
slotProps={{ htmlInput: { style: { fontSize: '12px', color: `${theme.palette.text.secondary}` } } }}
value={formatProperty(paladinTransaction.data[property])}
value={formatJSONWhenApplicable(paladinTransaction.data[property])}
/>
))}
{Object.keys(paladinTransaction.data).length === 0 &&
Expand Down
203 changes: 203 additions & 0 deletions ui/client/src/dialogs/ABIUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// 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,
Grid2,
Radio,
RadioGroup,
TextField,
Typography
} from '@mui/material';
import { FileUploader } from 'react-drag-drop-files';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UploadFile } from '@mui/icons-material';
import { useMutation } from '@tanstack/react-query';
import { uploadABI } from '../queries/storeABI';

type Props = {
dialogOpen: boolean
setDialogOpen: Dispatch<SetStateAction<boolean>>
}

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

const { t } = useTranslation();
const [errorMessage, setErrorMessage] = useState<string>();
const [fileSelected, setFileSelected] = useState<File | null>(null);
const [radioSelection, setRadioSelection] = useState<'file' | 'text'>('file');
const [abiText, setAbiText] = useState('');
const [abiUploadCount, setAbiUploadCount] = useState(0);

const { mutate, data, reset } = useMutation({
mutationFn: (value: Object) => uploadABI(value)
});

useEffect(() => {
if (dialogOpen) {
reset();
setRadioSelection('file');
setFileSelected(null);
setAbiText('');
}
}, [dialogOpen]);

const handleSubmit = async () => {
setErrorMessage(undefined);
reset();
let valueToParse: string;
let parsedValue: Object;
if (radioSelection === 'file' && fileSelected !== null) {
valueToParse = await fileSelected.text();
} else {
valueToParse = abiText;
}
try {
parsedValue = JSON.parse(valueToParse);
mutate(parsedValue);
setAbiUploadCount(abiUploadCount + 1);
} catch (err) {
if (err !== undefined) {
setErrorMessage(t('invalidABI'));
return;
}
}
};

const canSubmit = radioSelection === 'file' && fileSelected !== null
|| radioSelection === 'text' && abiText.length > 0;

return (
<Dialog
open={dialogOpen}
fullWidth
maxWidth="md"
onClose={() => setDialogOpen(false)}
>
<form onSubmit={(event) => {
event.preventDefault();
handleSubmit();
}}>
<DialogTitle sx={{ textAlign: 'center' }}>
{t('uploadABI')}
<Box sx={{ marginTop: '10px' }}>
{errorMessage !== undefined &&
<Alert variant="filled" severity="error">
{errorMessage}
</Alert>
}
{data !== undefined &&
<Alert variant="filled" severity="success">
{t('abiHash', { hash: data })}
</Alert>
}
</Box>
</DialogTitle>
<DialogContent>

<RadioGroup
value={radioSelection}
onChange={event => setRadioSelection(event.target.value as 'file' || 'text')}
>
<Grid2 container direction="column">
<Grid2>
<FormControlLabel value="file" control={<Radio />} label={t('uploadFile')} />
</Grid2>
<Grid2>
<FileUploader
disabled={radioSelection !== 'file'}
handleChange={(file: any) => {
setFileSelected(file);
}}
hoverTitle={t('dropFileHere')}
children={
<Box sx={{
display: 'flex',
height: '100px',
width: '100%',
padding: "10px",
borderRadius: '4px',
borderStyle: 'dashed',
cursor: radioSelection === 'file' ? 'pointer' : undefined,
alignItems: 'center',
justifyContent: 'center',
opacity: radioSelection !== 'file' ? '.4' : undefined
}}>
<UploadFile color="primary" />
<Typography align="center" fontWeight={500} sx={{ marginLeft: '4px' }}>
{fileSelected === null
? t('uploadABIFileDescription')
: t('abiFileSelected', { fileName: fileSelected.name })}
</Typography>
</Box>
}
types={['abi']}
/>
</Grid2>
<Grid2>
<Box sx={{ height: '25px' }} />
</Grid2>
<Grid2>
<FormControlLabel value="text" control={<Radio />} label={t('pasteABI')} />
</Grid2>
<Grid2>
<TextField
disabled={radioSelection !== 'text'}
fullWidth
multiline
rows={8}
value={abiText}
onChange={event => setAbiText(event.target.value)}
/>
</Grid2>
</Grid2>
</RadioGroup>
</DialogContent>
<DialogActions sx={{ justifyContent: 'center', paddingBottom: '20px' }}>
<Button
sx={{ minWidth: '100px' }}
size="large"
variant="contained"
disableElevation
disabled={!canSubmit}
type="submit">
{t('upload')}
</Button>
<Button
sx={{ minWidth: '100px' }}
size="large"
variant="outlined"
disableElevation
onClick={() => setDialogOpen(false)}
>
{t(abiUploadCount === 0 ? 'cancel' : 'close')}
</Button>
</DialogActions>
</form>
</Dialog>
);
};
Loading

0 comments on commit 7f9adcd

Please sign in to comment.