diff --git a/frontend/pages/stream/index.js b/frontend/pages/stream/index.js new file mode 100644 index 00000000..26fed40a --- /dev/null +++ b/frontend/pages/stream/index.js @@ -0,0 +1,68 @@ +import React, { useContext, useEffect } from "react"; +import { getLayout } from "../../src/layouts/EntriesLayout"; +import StreamEntryDetails from "../../src/components/SteamEntryDetails"; +import UIContext from "../../src/core/providers/UIProvider/context"; +import { + Box, + Heading, + Text, + Stack, + UnorderedList, + ListItem, +} from "@chakra-ui/react"; +import RouteButton from "../../src/components/RouteButton"; +const Entry = () => { + console.count("render stream!"); + const ui = useContext(UIContext); + + useEffect(() => { + if (typeof window !== "undefined") { + if (ui?.currentTransaction) { + document.title = `Stream details: ${ui.currentTransaction.hash}`; + } else { + document.title = `Stream`; + } + } + }, [ui?.currentTransaction]); + + if (ui?.currentTransaction) { + return ; + } else + return ( + + <> + + Stream view + + In this view you can follow events that happen on your subscribed + addresses + + + + Click filter icon on right top corner to filter by specific + address across your subscriptions + + + On event cards you can click at right corner to see detailed + view! + + + For any adress of interest here you can copy it and subscribe at + subscription screen + + + + Learn how to use moonstream + + + + + ); +}; +Entry.getLayout = getLayout; +export default Entry; diff --git a/frontend/src/components/EntriesNavigation.js b/frontend/src/components/EntriesNavigation.js new file mode 100644 index 00000000..e2230086 --- /dev/null +++ b/frontend/src/components/EntriesNavigation.js @@ -0,0 +1,520 @@ +import React, { useEffect, useContext, useState, useCallback } from "react"; +import { + Flex, + Spinner, + Button, + Center, + Text, + Menu, + MenuButton, + MenuList, + MenuItem, + MenuGroup, + IconButton, + Input, + Select, + Drawer, + DrawerBody, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + DrawerContent, + DrawerCloseButton, + useDisclosure, + Tag, + TagLabel, + TagCloseButton, + Stack, + Spacer, +} from "@chakra-ui/react"; +import { useSubscriptions } from "../core/hooks"; +import StreamEntry from "./StreamEntry"; +import UIContext from "../core/providers/UIProvider/context"; +import { FaFilter } from "react-icons/fa"; +import useStream from "../core/hooks/useStream"; +import { ImCancelCircle } from "react-icons/im"; +import { previousEvent } from "../core/services/stream.service"; +import { PAGE_SIZE } from "../core/constants"; +import DataContext from "../core/providers/DataProvider/context"; + +const FILTER_TYPES = { + ADDRESS: 0, + GAS: 1, + GAS_PRICE: 2, + AMOUNT: 3, + HASH: 4, + DISABLED: 99, +}; +const DIRECTIONS = { SOURCE: "from", DESTINATION: "to" }; +const CONDITION = { + EQUAL: 0, + CONTAINS: 1, + LESS: 2, + LESS_EQUAL: 3, + GREATER: 4, + GREATER_EQUAL: 5, + NOT_EQUAL: 6, +}; + +const EntriesNavigation = () => { + const { cursor, setCursor, streamCache, setStreamCache } = + useContext(DataContext); + const ui = useContext(UIContext); + const [firstLoading, setFirstLoading] = useState(true); + const { isOpen, onOpen, onClose } = useDisclosure(); + const { subscriptionsCache } = useSubscriptions(); + const [initialized, setInitialized] = useState(false); + const [newFilterState, setNewFilterState] = useState([ + { + type: FILTER_TYPES.ADDRESS, + direction: DIRECTIONS.SOURCE, + condition: CONDITION.EQUAL, + value: null, + }, + ]); + const [filterState, setFilterState] = useState([]); + + const { + eventsIsLoading, + eventsRefetch, + latestEventsRefetch, + nextEventRefetch, + previousEventRefetch, + streamBoundary, + setDefaultBoundary, + loadPreviousEventHandler, + loadNewesEventHandler, + loadOlderEventsIsFetching, + loadNewerEventsIsFetching, + previousEventIsFetching, + nextEventIsFetching, + olderEvent, + } = useStream( + ui.searchTerm.q, + streamCache, + setStreamCache, + cursor, + setCursor + ); + + useEffect(() => { + if (!streamBoundary.start_time && !streamBoundary.end_time) { + setDefaultBoundary(); + } else if (!initialized) { + eventsRefetch(); + latestEventsRefetch(); + nextEventRefetch(); + previousEventRefetch(); + setInitialized(true); + } else if ( + streamCache.length == 0 && + olderEvent?.event_timestamp && + firstLoading + ) { + loadPreviousEventHandler(); + setFirstLoading(false); + } + //TODO @AAndrey Dolgolev This useeffect produces lint warning, please review and + //Either add dependencies and remove comment line below, or add dependencies + //eslint-disable-next-line + }, [ + streamBoundary, + initialized, + setInitialized, + setDefaultBoundary, + eventsRefetch, + latestEventsRefetch, + nextEventRefetch, + previousEventRefetch, + ]); + + const setFilterProps = useCallback( + (filterIdx, props) => { + const newFilterProps = [...newFilterState]; + newFilterProps[filterIdx] = { ...newFilterProps[filterIdx], ...props }; + setNewFilterState(newFilterProps); + }, + [newFilterState, setNewFilterState] + ); + + useEffect(() => { + if ( + subscriptionsCache.data?.subscriptions[0]?.id && + newFilterState[0]?.value === null + ) { + setFilterProps(0, { + value: subscriptionsCache?.data?.subscriptions[0]?.address, + }); + } + }, [subscriptionsCache, newFilterState, setFilterProps]); + + const canCreate = false; + + const canDelete = false; + + const dropNewFilterArrayItem = (idx) => { + const oldArray = [...newFilterState]; + + const newArray = oldArray.filter(function (ele) { + return ele != oldArray[idx]; + }); + setNewFilterState(newArray); + }; + + const dropFilterArrayItem = (idx) => { + const oldArray = [...filterState]; + const newArray = oldArray.filter(function (ele) { + return ele != oldArray[idx]; + }); + + setFilterState(newArray); + setNewFilterState(newArray); + ui.setSearchTerm( + newArray + .map((filter) => { + return filter.direction + ":" + filter.value; + }) + .join("+") + ); + }; + + const handleFilterSubmit = () => { + setFilterState(newFilterState); + ui.setSearchTerm( + newFilterState + .map((filter) => { + return filter.direction + ":" + filter.value; + }) + .join("+") + ); + onClose(); + }; + + const handleAddressChange = (idx) => (e) => { + setFilterProps(idx, { value: e.target.value }); + }; + + const handleConditionChange = (idx) => (e) => { + setFilterProps(idx, { condition: parseInt(e.target.value) }); + }; + + const handleFilterStateCallback = (props) => { + const currentFilterState = [...filterState]; + currentFilterState.push({ ...props }); + + ui.setSearchTerm( + currentFilterState + .map((filter) => { + return filter.direction + ":" + filter.value; + }) + .join("+") + ); + + setFilterState(currentFilterState); + }; + if (subscriptionsCache.isLoading) return ""; + + return ( + + {streamCache && !eventsIsLoading ? ( + <> + + + + + {`Filter results`} + + + Source: + + {newFilterState.map((filter, idx) => { + if (filter.type === FILTER_TYPES.DISABLED) return ""; + return ( + + + {filter.type === FILTER_TYPES.ADDRESS && ( + <> + + {filter.direction === DIRECTIONS.SOURCE + ? `From:` + : `To:`} + + + {filter.direction === DIRECTIONS.SOURCE && ( + + )} + {filter.direction === DIRECTIONS.DESTINATION && ( + + setFilterProps(idx, { + value: e.target.value, + }) + } + placeholder="Type in address" + /> + )} + + )} + dropNewFilterArrayItem(idx)} + icon={} + /> + + + ); + })} + + + Add filter row + + + + + setNewFilterState([ + ...newFilterState, + { + type: FILTER_TYPES.ADDRESS, + direction: DIRECTIONS.SOURCE, + condition: CONDITION.EQUAL, + value: + subscriptionsCache?.data?.subscriptions[0] + ?.address, + }, + ]) + } + > + Source + + + setNewFilterState([ + ...newFilterState, + { + type: FILTER_TYPES.ADDRESS, + direction: DIRECTIONS.DESTINATION, + condition: CONDITION.EQUAL, + value: + subscriptionsCache?.data?.subscriptions[0] + ?.address, + }, + ]) + } + > + Destination + + + + + + + + + + + + {filterState.map((filter, idx) => { + if (filter.type === FILTER_TYPES.DISABLED) return ""; + return ( + + {filter?.type === FILTER_TYPES.ADDRESS && ( + + {filter.condition === CONDITION.NOT_EQUAL && "Not "} + {filter.direction === DIRECTIONS.SOURCE + ? "From: " + : "To: "} + {subscriptionsCache?.data?.subscriptions.find( + (subscription) => + subscription.address === filter.value + )?.label ?? filter.value} + + )} + + dropFilterArrayItem(idx)} /> + + ); + })} + + + } + /> + + + + handleScroll(e)} + > + + {!loadNewerEventsIsFetching && !nextEventIsFetching ? ( + + ) : ( + + )} + + {streamCache + .slice( + cursor, + streamCache.length <= cursor + PAGE_SIZE + ? streamCache.length + : cursor + PAGE_SIZE + ) + .map((entry, idx) => ( + + ))} + {previousEvent && + !loadOlderEventsIsFetching && + !previousEventIsFetching ? ( +
+ +
+ ) : ( +
+ {!previousEventIsFetching && !loadOlderEventsIsFetching ? ( + "Тransactions not found. You can subscribe to more addresses in Subscriptions menu." + ) : ( + + )} +
+ )} +
+
+ + ) : ( +
+ +
+ )} +
+ ); +}; + +export default EntriesNavigation; diff --git a/frontend/src/components/Report.js b/frontend/src/components/Report.js index e8b9fe00..82bc43ad 100644 --- a/frontend/src/components/Report.js +++ b/frontend/src/components/Report.js @@ -1,7 +1,7 @@ import React from "react"; import { ResponsiveLineCanvas } from "@nivo/line"; -const Report = ({ data, metric, timeRange }) => { +const Report = ({ data, timeRange }) => { const commonProperties = { animate: false, enableSlices: "x", diff --git a/frontend/src/components/Sidebar.js b/frontend/src/components/Sidebar.js index 5b2e6eed..cdc0068e 100644 --- a/frontend/src/components/Sidebar.js +++ b/frontend/src/components/Sidebar.js @@ -24,7 +24,7 @@ import { ArrowRightIcon, LockIcon, } from "@chakra-ui/icons"; -import { MdSettings, MdDashboard } from "react-icons/md"; +import { MdSettings, MdDashboard, MdTimeline } from "react-icons/md"; import { WHITE_LOGO_W_TEXT_URL, ALL_NAV_PATHES } from "../core/constants"; import { v4 } from "uuid"; import useDashboard from "../core/hooks/useDashboard"; @@ -177,6 +177,9 @@ const Sidebar = () => { }> Subscriptions + }> + Stream + { + const ui = useContext(UIContext); + const defaultWidth = useBreakpointValue({ + base: "14rem", + sm: "16rem", + md: "18rem", + lg: "20rem", + xl: "22rem", + "2xl": "24rem", + }); + + return ( + <> + + + + + + + + {props.children} + + + + + ); +}; + +export const getLayout = (page) => + getSiteLayout({page}); + +export default EntriesLayout;