Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infinite scrolling #411

Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions ui/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.2",
"react-infinite-scroll-component": "^6.1.0",
"react-json-pretty": "^2.2.0",
"react-router-dom": "^6.26.2"
},
Expand Down
42 changes: 30 additions & 12 deletions ui/client/src/components/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,67 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { Alert, Box, Typography, useTheme } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { Alert, Box, LinearProgress, Typography, useTheme } from "@mui/material";
import { useInfiniteQuery } from "@tanstack/react-query";
import { t } from "i18next";
import { fetchEvents } from "../queries/events";
import { Event } from "./Event";
import { useContext } from "react";
import { ApplicationContext } from "../contexts/ApplicationContext";
import { altDarkModeScrollbarStyle, altLightModeScrollbarStyle } from "../themes/default";
import InfiniteScroll from "react-infinite-scroll-component";
import { IEvent } from "../interfaces";

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

const { lastBlockWithTransactions } = useContext(ApplicationContext);
const theme = useTheme();
const addedStyle = theme.palette.mode === 'light'? altLightModeScrollbarStyle : altDarkModeScrollbarStyle;

const { data: events, error, isFetching } = useQuery({
const { data: events, fetchNextPage, hasNextPage, error } = useInfiniteQuery({
queryKey: ["events", lastBlockWithTransactions],
queryFn: () => fetchEvents(),
queryFn: ({ pageParam }) => fetchEvents(pageParam),
initialPageParam: undefined as IEvent | undefined,
getNextPageParam: (lastPage) => {return lastPage[lastPage.length - 1]},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for lastPage.length === 0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, just like the case above, this has been updated to:

getNextPageParam: (lastPage) => { return lastPage.length > 0? lastPage[lastPage.length - 1] : undefined },

});

if(isFetching) {
return <></>;
}
const theme = useTheme();
const addedStyle = theme.palette.mode === 'light' ? altLightModeScrollbarStyle : altDarkModeScrollbarStyle;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think this should be moved to a utility function or a hook

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I moved it into an utility function (just 1 line) and applied it to every place where this was being used.


if (error) {
return <Alert sx={{ margin: '30px' }} severity="error" variant="filled">{error.message}</Alert>
}

if (events?.pages === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use skeleton loader eventually

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely agree, we would need to work together on a design that allows a loader to be shown without running into the issue of showing up and disappearing immediately which makes it appear as if the screen flickers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I think the Event card can have a isLoading prop. Then inside the card, skeletons can show for each piece of data. That way, the components still load, but the content changes from a skeleton to text.

return <></>;
}

return (
<>
<Typography align="center" variant="h5" sx={{ marginBottom: '20px' }}>
{t("events")}
</Typography>
<Box
id="scrollableDivEvents"
sx={{
height: "calc(100vh - 170px)",
paddingRight: "15px",
...addedStyle
}}
>
{events?.map((event) => (
<Event key={`${event.blockNumber}-${event.logIndex}`} event={event} />
))}
<InfiniteScroll
scrollableTarget="scrollableDivEvents"
dataLength={events.pages.length}
next={() => fetchNextPage()}
hasMore={hasNextPage}
loader={<LinearProgress />}
>
{
events.pages.map(eventArray => eventArray.map(
(event) => (
<Event key={`${event.blockNumber}-${event.logIndex}`} event={event} />
)
))
}
</InfiniteScroll>
</Box>
</>
);
Expand Down
95 changes: 70 additions & 25 deletions ui/client/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { AppBar, Box, Grid2, IconButton, Tab, Tabs, Toolbar, Tooltip, useMediaQuery, useTheme } from "@mui/material";
import { AppBar, Box, Button, Grid2, IconButton, Tab, Tabs, ToggleButton, ToggleButtonGroup, Toolbar, Tooltip, 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';

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

const { colorMode } = useContext(ApplicationContext);
const { colorMode, autoRefreshEnabled, setAutoRefreshEnabled, refreshRequired, refresh } = useContext(ApplicationContext);
const { t } = useTranslation();
const navigate = useNavigate();
const pathname = useLocation().pathname.toLowerCase();
Expand Down Expand Up @@ -53,37 +55,80 @@ export const Header: React.FC = () => {
}
};

const handleAutoRefreshChange = (value: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can value be of type 'play' | 'pause' instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, updated the code to

const handleAutoRefreshChange = (value: 'play' | 'pause') => {

switch (value) {
case 'play': setAutoRefreshEnabled(true); break;
case 'pause': setAutoRefreshEnabled(false); break;
}
};

return (
<>
<AppBar>
<Toolbar sx={{ backgroundColor: theme => theme.palette.background.paper }}>
<Box sx={{ width: '100%', maxWidth: '1270px', marginLeft: 'auto', marginRight: 'auto' }}>
<Grid2 container alignItems="center" >
<Grid2 size={{ xs: 12, sm: 12, md: 4 }}>
<img src={theme.palette.mode === 'dark' ?
'/ui/paladin-title-dark.svg' : '/ui/paladin-title-light.svg'
} style={{ marginTop: '7px' }} />
</Grid2>
<Grid2 size={{ xs: 12, sm: 12, md: 4 }} alignContent="center">
<Tabs value={tab} onChange={(_event, value) => handleNavigation(value)} centered>
<Tab sx={{ textTransform: 'none' }} label={t('indexer')} />
<Tab sx={{ textTransform: 'none' }} label={t('submissions')} />
<Tab sx={{ textTransform: 'none' }} label={t('registry')} />
</Tabs>
</Grid2>
<Grid2 size={{ xs: 12, sm: 12, md: 4 }} textAlign="right">
<Tooltip arrow title={t('switchThemeMode')}>
<IconButton onClick={() => colorMode.toggleColorMode()}>
<Brightness4Icon />
</IconButton>
</Tooltip>
<Grid2 container alignItems="center">
<Grid2 size={{ xs: 12, sm: 12, md: 4 }} textAlign={lessThanMedium ? 'center' : 'left'}>
<img src={theme.palette.mode === 'dark' ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are probably many instances of this ternary. I suggest having a util function:

export const getThemedComponent = (lightComponent, darkComponent, theme) => {
   return theme === 'light' ? lightComponent : darkComponent
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can certainly add the utility. For me to better understand the snippet above, would the lightComponent and darkComponent argument be JSX.Elements?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be JSX.Element | string to start with

'/ui/paladin-title-dark.svg' : '/ui/paladin-title-light.svg'
} style={{ marginTop: '7px' }} />
</Grid2>
<Grid2 size={{ xs: 12, sm: 12, md: 4 }} alignContent="center">
<Tabs
TabIndicatorProps={{ style: { height: '4px' } }}
value={tab} onChange={(_event, value) => handleNavigation(value)} centered>
<Tab sx={{ textTransform: 'none' }} label={t('indexer')} />
<Tab sx={{ textTransform: 'none' }} label={t('submissions')} />
<Tab sx={{ textTransform: 'none' }} label={t('registry')} />
</Tabs>
</Grid2>
<Grid2 size={{ xs: 12, sm: 12, md: 4 }}>
<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>
<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>
</Grid2>
</Grid2>
</Grid2>
</Grid2>
</Grid2>
</Box>
</Toolbar>
</AppBar>
<Box sx={{ height: theme => lessThanMedium? '134px' :
theme.mixins.toolbar }} />
<Box sx={{
height: theme => lessThanMedium ? '190px' :
theme.mixins.toolbar
}} />
</>
);

Expand Down
2 changes: 1 addition & 1 deletion ui/client/src/components/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const Transaction: React.FC<Props> = ({

const [viewDetailsDialogOpen, setViewDetailsDialogOpen] = useState(false);
const receiptCount = (transactionReceipts && transactionReceipts.length) ? transactionReceipts.length : 0;
const receiptIsPrivate = (transactionReceipts && transactionReceipts.length && transactionReceipts[0].domain !== '');
const receiptIsPrivate = (transactionReceipts && transactionReceipts.length && transactionReceipts[0].domain !== undefined);
const typeKey =
receiptCount > 1 ? 'atomicNumber' :
receiptIsPrivate ? 'private' :
Expand Down
Loading