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:`}
+
+
+ Is
+
+ Is not
+
+
+ {filter.direction === DIRECTIONS.SOURCE && (
+
+ {!subscriptionsCache.isLoading &&
+ subscriptionsCache?.data?.subscriptions.map(
+ (subscription, idx) => {
+ return (
+
+ {`${
+ subscription.label
+ } - ${subscription.address.slice(
+ 0,
+ 5
+ )}...${subscription.address.slice(
+ -3
+ )}`}
+
+ );
+ }
+ )}
+
+ )}
+ {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
+
+
+
+
+
+ handleFilterSubmit()}
+ >
+ Apply selected filters
+
+
+
+
+
+
+ {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 ? (
+ {
+ loadNewesEventHandler();
+ }}
+ variant="outline"
+ colorScheme="green"
+ >
+ Load newer events
+
+ ) : (
+
+ )}
+
+ {streamCache
+ .slice(
+ cursor,
+ streamCache.length <= cursor + PAGE_SIZE
+ ? streamCache.length
+ : cursor + PAGE_SIZE
+ )
+ .map((entry, idx) => (
+
+ ))}
+ {previousEvent &&
+ !loadOlderEventsIsFetching &&
+ !previousEventIsFetching ? (
+
+ {
+ loadPreviousEventHandler();
+ }}
+ variant="outline"
+ colorScheme="green"
+ >
+ Load older events
+
+
+ ) : (
+
+ {!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;