diff --git a/src/@types/parseable/api/query.ts b/src/@types/parseable/api/query.ts index e008bc94..67ea9b72 100644 --- a/src/@types/parseable/api/query.ts +++ b/src/@types/parseable/api/query.ts @@ -32,3 +32,8 @@ export type Log = { p_tags: string; [key: string]: string | number | null | Date; }; + +export type LogSelectedTimeRange = { + state : "fixed"| "custom"; + value : string; +}; diff --git a/src/components/Header/HeaderBreadcrumbs.tsx b/src/components/Header/HeaderBreadcrumbs.tsx new file mode 100644 index 00000000..b22a1cc9 --- /dev/null +++ b/src/components/Header/HeaderBreadcrumbs.tsx @@ -0,0 +1,53 @@ +import useMountedState from '@/hooks/useMountedState'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import { Breadcrumbs, Text } from '@mantine/core'; +import type { FC } from 'react'; +import { useEffect } from 'react'; +import { useLogQueryStyles } from './styles'; + +type HeaderBreadcrumbs = { + crumbs: string[]; +}; + +const HeaderBreadcrumbs: FC = (props) => { + const { crumbs } = props; + const { + state: { subLogQuery }, + } = useHeaderContext(); + const [streamName, setStreamName] = useMountedState(subLogQuery.get().streamName); + + useEffect(() => { + const listener = subLogQuery.subscribe((state) => { + setStreamName(state.streamName); + }); + return () => listener(); + }, []); + + return ( + + + {crumbs.map((crumb) => { + if (crumb === 'streamName') { + return {streamName}; + } + + return {crumb}; + })} + + ); +}; + +const HomeIcon: FC = () => { + const { classes } = useLogQueryStyles(); + const { homeIcon } = classes; + return ( + + + + ); +}; + +export default HeaderBreadcrumbs; diff --git a/src/components/Header/Layout.tsx b/src/components/Header/Layout.tsx new file mode 100644 index 00000000..ed440b4a --- /dev/null +++ b/src/components/Header/Layout.tsx @@ -0,0 +1,30 @@ +import logoInvert from '@/assets/images/brand/logo-invert.svg'; +import { HOME_ROUTE } from '@/constants/routes'; +import { HEADER_HEIGHT } from '@/constants/theme'; +import type { HeaderProps as MantineHeaderProps } from '@mantine/core'; +import { Box, Image, Header as MantineHeader } from '@mantine/core'; +import { FC } from 'react'; +import { Link, Outlet } from 'react-router-dom'; +import { useHeaderStyles } from './styles'; + +type HeaderProps = Omit; + +const HeaderLayout: FC = (props) => { + const { classes } = useHeaderStyles(); + const { container, logoContainer, navContainer, imageSty } = classes; + + return ( + + + + Parseable Logo + + + + + + + ); +}; + +export default HeaderLayout; diff --git a/src/components/Header/RefreshInterval.tsx b/src/components/Header/RefreshInterval.tsx new file mode 100644 index 00000000..c8661a9f --- /dev/null +++ b/src/components/Header/RefreshInterval.tsx @@ -0,0 +1,62 @@ +import useMountedState from '@/hooks/useMountedState'; +import { REFRESH_INTERVALS, useHeaderContext } from '@/layouts/MainLayout/Context'; +import { Button, Menu, Text, px } from '@mantine/core'; +import { IconRefresh, IconRefreshOff } from '@tabler/icons-react'; +import ms from 'ms'; +import type { FC } from 'react'; +import { useEffect, useMemo } from 'react'; +import { useLogQueryStyles } from './styles'; + +const RefreshInterval: FC = () => { + const { + state: { subRefreshInterval }, + } = useHeaderContext(); + + const [selectedInterval, setSelectedInterval] = useMountedState(subRefreshInterval.get()); + + useEffect(() => { + const listener = subRefreshInterval.subscribe((interval) => { + setSelectedInterval(interval); + }); + + return () => listener(); + }, []); + + const Icon = useMemo(() => (selectedInterval ? IconRefresh : IconRefreshOff), [selectedInterval]); + + const onSelectedInterval = (interval: number | null) => { + subRefreshInterval.set(interval); + }; + + const { classes } = useLogQueryStyles(); + const { intervalBtn } = classes; + + return ( + + + + + + {REFRESH_INTERVALS.map((interval) => { + if (interval === selectedInterval) return null; + + return ( + onSelectedInterval(interval)}> + {ms(interval)} + + ); + })} + + {selectedInterval !== null && ( + onSelectedInterval(null)}> + Off + + )} + + + ); +}; + +export default RefreshInterval; diff --git a/src/components/Header/RefreshNow.tsx b/src/components/Header/RefreshNow.tsx new file mode 100644 index 00000000..55da75f1 --- /dev/null +++ b/src/components/Header/RefreshNow.tsx @@ -0,0 +1,33 @@ +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import { Button, px } from '@mantine/core'; +import { IconReload } from '@tabler/icons-react'; +import dayjs from 'dayjs'; +import type { FC } from 'react'; +import { useLogQueryStyles } from './styles'; + +const RefreshNow: FC = () => { + const { + state: { subLogQuery, subLogSelectedTimeRange }, + } = useHeaderContext(); + + const onRefresh = () => { + if (subLogSelectedTimeRange.get().state==='fixed') { + const now = dayjs(); + const timeDiff = subLogQuery.get().endTime.getTime() - subLogQuery.get().startTime.getTime(); + subLogQuery.set((state) => { + state.startTime = now.subtract(timeDiff).toDate(); + state.endTime = now.toDate(); + }); + } + }; + const { classes } = useLogQueryStyles(); + const { refreshNowBtn } = classes; + + return ( + + ); +}; + +export default RefreshNow; diff --git a/src/components/Header/Search.tsx b/src/components/Header/Search.tsx new file mode 100644 index 00000000..f0c9ee5f --- /dev/null +++ b/src/components/Header/Search.tsx @@ -0,0 +1,58 @@ +import useMountedState from '@/hooks/useMountedState'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import { Box, TextInput, px } from '@mantine/core'; +import { IconSearch } from '@tabler/icons-react'; +import type { ChangeEvent, FC, KeyboardEvent } from 'react'; +import { useEffect } from 'react'; +import { useLogQueryStyles } from './styles'; + +const Search: FC = () => { + const { + state: { subLogSearch }, + } = useHeaderContext(); + + const [searchValue, setSearchValue] = useMountedState(''); + + useEffect(() => { + const listener = subLogSearch.subscribe((interval) => { + setSearchValue(interval.search); + }); + return () => { + listener(); + }; + }, []); + + const { classes } = useLogQueryStyles(); + const { searchContainer, searchInput } = classes; + + const onSearchValueChange = (event: ChangeEvent) => { + setSearchValue(event.currentTarget.value); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + if (subLogSearch.get().search !== searchValue) { + const trimmedValue = event.currentTarget.value.trim(); + subLogSearch.set((query) => { + query.search = trimmedValue; + }); + setSearchValue(trimmedValue); + } + } + }; + + return ( + + } + /> + + ); +}; + +export default Search; diff --git a/src/components/Header/SubHeader.tsx b/src/components/Header/SubHeader.tsx index c713113e..e37ca3b9 100644 --- a/src/components/Header/SubHeader.tsx +++ b/src/components/Header/SubHeader.tsx @@ -1,358 +1,90 @@ -import useMountedState from '@/hooks/useMountedState'; -import { Box, Breadcrumbs, Button, Menu, Text, TextInput, UnstyledButton, px } from '@mantine/core'; -import { DateTimePicker } from '@mantine/dates'; -import { IconClock, IconRefresh, IconReload, IconRefreshOff, IconSearch } from '@tabler/icons-react'; -import dayjs from 'dayjs'; -import ms from 'ms'; -import type { ChangeEvent, FC, KeyboardEvent } from 'react'; -import { Fragment, useEffect, useMemo } from 'react'; -import { FIXED_DURATIONS, REFRESH_INTERVALS, useHeaderContext } from '@/layouts/MainLayout/Context'; +import { Box } from '@mantine/core'; +import type { FC } from 'react'; +import HeaderBreadcrumbs from './HeaderBreadcrumbs'; +import RefreshInterval from './RefreshInterval'; +import RefreshNow from './RefreshNow'; +import Search from './Search'; +import TimeRange from './TimeRange'; import { useLogQueryStyles } from './styles'; -import { useMatch } from 'react-router-dom'; -const SubHeader: FC = () => { - const { classes } = useLogQueryStyles(); - const { container, innerContainer, homeIcon } = classes; - const { - state: { subLogQuery }, - } = useHeaderContext(); - const [streamName, setStreamName] = useMountedState(subLogQuery.get().streamName); - useEffect(() => { - const listener = subLogQuery.subscribe((state) => { - setStreamName(state.streamName); - }); - return () => listener(); - }, []); +export const StatsHeader: FC = () => { + const { classes } = useLogQueryStyles(); + const { container, innerContainer } = classes; return ( - - - - - Streams - {streamName} - {useMatch('/:streamName/stats') && Stats } - - {useMatch('/:streamName/logs') && Logs } - - {useMatch('/:streamName/query') && Query } - - {useMatch('/:streamName/config') && Config } - + - {useMatch('/:streamName/stats') && } - - {useMatch('/:streamName/logs') && <> - - - - - } - - {useMatch('/:streamName/query') && <> - - - } - + ); }; -const Search: FC = () => { - const { - state: { subLogSearch }, - } = useHeaderContext(); - - const [searchValue, setSearchValue] = useMountedState(''); +export const QueryHeader: FC = () => { const { classes } = useLogQueryStyles(); - - useEffect(() => { - const listener = subLogSearch.subscribe((interval) => { - setSearchValue(interval.search); - }); - return () => { - listener(); - }; - }, []); - const { searchContainer, searchInput } = classes; - - const onSearchValueChange = (event: ChangeEvent) => { - setSearchValue(event.currentTarget.value); - }; - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Enter') { - if (subLogSearch.get().search !== searchValue) { - const trimmedValue = event.currentTarget.value.trim(); - subLogSearch.set((query) => { - query.search = trimmedValue; - }); - setSearchValue(trimmedValue); - } - } - }; + const { container, innerContainer } = classes; return ( - - {/* TODO: Disabled for now, need to find a proper way to handle SQL search */} - {/* */} - } - /> - - ); -}; - -const RefreshNow: FC = () => { - const { - state: { subLogQuery, subLogSelectedTimeRange }, - } = useHeaderContext(); - - const onRefresh = () => { - if (subLogSelectedTimeRange.get().includes('last')) { - const now = dayjs(); - const timeDiff = subLogQuery.get().endTime.getTime() - subLogQuery.get().startTime.getTime(); - subLogQuery.set((state) => { - state.startTime = now.subtract(timeDiff).toDate(); - state.endTime = now.toDate(); - }); - } - }; - const { classes } = useLogQueryStyles(); - const { refreshNowBtn } = classes; + + + + + + - return ( - + + + + + + + ); }; -const RefreshInterval: FC = () => { - const { - state: { subRefreshInterval }, - } = useHeaderContext(); - - const [selectedInterval, setSelectedInterval] = useMountedState(subRefreshInterval.get()); - - useEffect(() => { - const listener = subRefreshInterval.subscribe((interval) => { - setSelectedInterval(interval); - }); - - return () => listener(); - }, []); - - const Icon = useMemo(() => (selectedInterval ? IconRefresh : IconRefreshOff), [selectedInterval]); - - const onSelectedInterval = (interval: number | null) => { - subRefreshInterval.set(interval); - }; - +export const LogsHeader: FC = () => { const { classes } = useLogQueryStyles(); - const { intervalBtn } = classes; + const { container, innerContainer } = classes; return ( - - - - - - {REFRESH_INTERVALS.map((interval) => { - if (interval === selectedInterval) return null; - - return ( - onSelectedInterval(interval)}> - {ms(interval)} - - ); - })} - - {selectedInterval !== null && ( - onSelectedInterval(null)}> - Off - - )} - - - ); -}; - -type FixedDurations = (typeof FIXED_DURATIONS)[number]; - -const TimeRange: FC = () => { - const { - state: { subLogQuery, subLogSelectedTimeRange }, - } = useHeaderContext(); - - const [selectedRange, setSelectedRange] = useMountedState(subLogSelectedTimeRange.get()); - - useEffect(() => { - const listener = subLogSelectedTimeRange.subscribe((state) => { - setSelectedRange(state); - }); - - return () => listener(); - }, []); - - const onDurationSelect = (duration: FixedDurations) => { - subLogSelectedTimeRange.set(duration.name); - const now = dayjs(); - - subLogQuery.set((query) => { - query.startTime = now.subtract(duration.milliseconds, 'milliseconds').toDate(); - query.endTime = now.toDate(); - }); - }; - - const { classes, cx } = useLogQueryStyles(); - const { - timeRangeBTn, - timeRangeContainer, - fixedRangeContainer, - fixedRangeBtn, - fixedRangeBtnSelected, - customRangeContainer, - } = classes; + + + + + + - return ( - - - - - - - - {FIXED_DURATIONS.map((duration) => { - return ( - onDurationSelect(duration)}> - {duration.name} - - ); - })} - - - - + + + + + + - - + + ); }; -const CustomTimeRange: FC = () => { - const { - state: { subLogQuery, subLogSelectedTimeRange }, - } = useHeaderContext(); - - const [selectedRange, setSelectedRange] = useMountedState({ - startTime: subLogQuery.get().startTime, - endTime: subLogQuery.get().endTime, - }); - - const onRangeSelect = (key: keyof typeof selectedRange, date: Date) => { - setSelectedRange((state) => { - state[key] = date; - return { ...state }; - }); - }; - - const onApply = () => { - subLogQuery.set((query) => { - query.startTime = selectedRange.startTime; - query.endTime = selectedRange.endTime; - }); - const startTime = dayjs(selectedRange.startTime).format('DD-MM-YY HH:mm'); - const endTime = dayjs(selectedRange.endTime).format('DD-MM-YY HH:mm'); - subLogSelectedTimeRange.set(`${startTime} - ${endTime}`); - }; - +export const ConfigHeader: FC = () => { const { classes } = useLogQueryStyles(); - const { customTimeRangeFooter, customTimeRangeApplyBtn } = classes; - - const isApplicable = useMemo(() => { - return ( - dayjs(selectedRange.startTime).isSame(subLogQuery.get().startTime, 'seconds') && - dayjs(selectedRange.endTime).isSame(subLogQuery.get().endTime, 'seconds') - ); - }, [selectedRange]); - - const isStartTimeMoreThenEndTime = useMemo(() => { - return dayjs(selectedRange.startTime).isAfter(selectedRange.endTime, 'seconds'); - }, [selectedRange]); + const { container, innerContainer } = classes; return ( - - { - if (date) { - onRangeSelect('startTime', date); - } - }} - valueFormat="DD-MM-YY HH:mm" - label="From" - placeholder="Pick date and time" - /> - { - if (date) { - onRangeSelect('endTime', date); - } - }} - valueFormat="DD-MM-YY HH:mm" - label="To" - placeholder="Pick date and time" - mt="md" - /> - - + + + + + - + ); }; - -export default SubHeader; diff --git a/src/components/Header/TimeRange.tsx b/src/components/Header/TimeRange.tsx new file mode 100644 index 00000000..7ba42cc2 --- /dev/null +++ b/src/components/Header/TimeRange.tsx @@ -0,0 +1,169 @@ +import useMountedState from '@/hooks/useMountedState'; +import { FIXED_DURATIONS, useHeaderContext } from '@/layouts/MainLayout/Context'; +import { Box, Button, Menu, Text, UnstyledButton, px } from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; +import { IconClock } from '@tabler/icons-react'; +import dayjs from 'dayjs'; +import type { FC } from 'react'; +import { Fragment, useEffect, useMemo } from 'react'; +import { useLogQueryStyles } from './styles'; + +type FixedDurations = (typeof FIXED_DURATIONS)[number]; + +const TimeRange: FC = () => { + const { + state: { subLogQuery, subLogSelectedTimeRange }, + } = useHeaderContext(); + + const [selectedRange, setSelectedRange] = useMountedState(subLogSelectedTimeRange.get().value); + + useEffect(() => { + const listener = subLogSelectedTimeRange.subscribe((state) => { + setSelectedRange(state.value); + }); + + return () => listener(); + }, []); + + const onDurationSelect = (duration: FixedDurations) => { + subLogSelectedTimeRange.set((state)=>{ + state.value = duration.name; + state.state = 'fixed'; + }); + const now = dayjs(); + + subLogQuery.set((query) => { + query.startTime = now.subtract(duration.milliseconds, 'milliseconds').toDate(); + query.endTime = now.toDate(); + }); + }; + + const { classes, cx } = useLogQueryStyles(); + const { + timeRangeBTn, + timeRangeContainer, + fixedRangeContainer, + fixedRangeBtn, + fixedRangeBtnSelected, + customRangeContainer, + } = classes; + + return ( + + + + + + + + {FIXED_DURATIONS.map((duration) => { + return ( + onDurationSelect(duration)}> + {duration.name} + + ); + })} + + + + + + + + ); +}; + +const CustomTimeRange: FC = () => { + const { + state: { subLogQuery, subLogSelectedTimeRange }, + } = useHeaderContext(); + + const [selectedRange, setSelectedRange] = useMountedState({ + startTime: subLogQuery.get().startTime, + endTime: subLogQuery.get().endTime, + }); + + const onRangeSelect = (key: keyof typeof selectedRange, date: Date) => { + setSelectedRange((state) => { + state[key] = date; + return { ...state }; + }); + }; + + const onApply = () => { + subLogQuery.set((query) => { + query.startTime = selectedRange.startTime; + query.endTime = selectedRange.endTime; + }); + const startTime = dayjs(selectedRange.startTime).format('DD-MM-YY HH:mm'); + const endTime = dayjs(selectedRange.endTime).format('DD-MM-YY HH:mm'); + subLogSelectedTimeRange.set((state)=>{ + state.state = 'custom'; + state.value = `${startTime} - ${endTime}`; + }); + }; + + const { classes } = useLogQueryStyles(); + const { customTimeRangeFooter, customTimeRangeApplyBtn } = classes; + + const isApplicable = useMemo(() => { + return ( + dayjs(selectedRange.startTime).isSame(subLogQuery.get().startTime, 'seconds') && + dayjs(selectedRange.endTime).isSame(subLogQuery.get().endTime, 'seconds') + ); + }, [selectedRange]); + + const isStartTimeMoreThenEndTime = useMemo(() => { + return dayjs(selectedRange.startTime).isAfter(selectedRange.endTime, 'seconds'); + }, [selectedRange]); + + return ( + + { + if (date) { + onRangeSelect('startTime', date); + } + }} + valueFormat="DD-MM-YY HH:mm" + label="From" + placeholder="Pick date and time" + /> + { + if (date) { + onRangeSelect('endTime', date); + } + }} + valueFormat="DD-MM-YY HH:mm" + label="To" + placeholder="Pick date and time" + mt="md" + /> + + + + + ); +}; + +export default TimeRange; diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 7b4a8598..1d0feeb6 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,48 +1,22 @@ -import logoInvert from '@/assets/images/brand/logo-invert.svg'; -import { HOME_ROUTE } from '@/constants/routes'; -import { HEADER_HEIGHT } from '@/constants/theme'; import type { HeaderProps as MantineHeaderProps } from '@mantine/core'; -import { Box, Image, Header as MantineHeader } from '@mantine/core'; import { FC } from 'react'; -import { Link } from 'react-router-dom'; -import { useHeaderStyles } from './styles'; -import SubHeader from './SubHeader'; -// import { useHeaderContext } from '@/layouts/MainLayout/Context'; -// import useMountedState from '@/hooks/useMountedState'; +import { Route, Routes } from 'react-router-dom'; +import HeaderLayout from './Layout'; +import { ConfigHeader, LogsHeader, QueryHeader, StatsHeader } from './SubHeader'; +import { CONFIG_ROUTE, LOGS_ROUTE, QUERY_ROUTE, STATS_ROUTE } from '@/constants/routes'; type HeaderProps = Omit; const Header: FC = (props) => { - const { classes } = useHeaderStyles(); - const { container, logoContainer, navContainer, imageSty } = classes; - // const { - // state: { subNavbarTogle }, - // } = useHeaderContext(); - // const [isSubNavbarOpen, setIsSubNavbarOpen] = useMountedState(false); - // useEffect(() => { - // const listener = subNavbarTogle.subscribe(setIsSubNavbarOpen); - // return () => { - // listener(); - // }; - // }, []); - return ( - - - - Parseable Logo - - {/* subNavbarTogle.set((state) => !state)} - size={24} - /> */} - - - - - + + }> + } /> + } /> + } /> + } /> + + ); }; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 89e538f0..24008153 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -136,7 +136,10 @@ const Navbar: FC = (props) => { state.startTime = now.subtract(DEFAULT_FIXED_DURATIONS.milliseconds, 'milliseconds').toDate(); state.endTime = now.toDate(); }); - subLogSelectedTimeRange.set(DEFAULT_FIXED_DURATIONS.name); + subLogSelectedTimeRange.set((state) => { + state.state = 'fixed'; + state.value = DEFAULT_FIXED_DURATIONS.name; + }); subLogSearch.set((state) => { state.search = ''; state.filters = {}; diff --git a/src/layouts/MainLayout/Context.tsx b/src/layouts/MainLayout/Context.tsx index 78375221..f238fa2a 100644 --- a/src/layouts/MainLayout/Context.tsx +++ b/src/layouts/MainLayout/Context.tsx @@ -1,4 +1,4 @@ -import { SortOrder, type LogsQuery, type LogsSearch } from '@/@types/parseable/api/query'; +import { SortOrder, type LogsQuery, type LogsSearch, type LogSelectedTimeRange } from '@/@types/parseable/api/query'; import useSubscribeState, { SubData } from '@/hooks/useSubscribeState'; import dayjs from 'dayjs'; import type { FC } from 'react'; @@ -47,7 +47,7 @@ interface HeaderContextState { subLogQuery: SubData; subLogSearch: SubData; subRefreshInterval: SubData; - subLogSelectedTimeRange: SubData; + subLogSelectedTimeRange: SubData; subNavbarTogle: SubData; } @@ -77,7 +77,10 @@ const MainLayoutPageProvider: FC = ({ children }) => { order: SortOrder.DESCENDING } }); - const subLogSelectedTimeRange = useSubscribeState(DEFAULT_FIXED_DURATIONS.name); + const subLogSelectedTimeRange = useSubscribeState({ + state: "fixed", + value: DEFAULT_FIXED_DURATIONS.name, + }); const subRefreshInterval = useSubscribeState(null); const subNavbarTogle = useSubscribeState(false); diff --git a/src/pages/Logs/LogTable.tsx b/src/pages/Logs/LogTable.tsx index 3b64282b..e4517993 100644 --- a/src/pages/Logs/LogTable.tsx +++ b/src/pages/Logs/LogTable.tsx @@ -145,7 +145,7 @@ const LogTable: FC = () => { useEffect(() => { if (subRefreshInterval.get()) { const interval = setInterval(() => { - if (subLogSelectedTimeRange.get().includes('Past')) { + if (subLogSelectedTimeRange.get().state === 'fixed') { const now = dayjs(); const timeDiff = subLogQuery.get().endTime.getTime() - subLogQuery.get().startTime.getTime(); subLogQuery.set((state) => { diff --git a/src/pages/Query/QueryCodeEditor.tsx b/src/pages/Query/QueryCodeEditor.tsx index 8c382890..cba41272 100644 --- a/src/pages/Query/QueryCodeEditor.tsx +++ b/src/pages/Query/QueryCodeEditor.tsx @@ -92,7 +92,7 @@ useEffect(() => { endTime :subLogQuery.get().endTime, streamName : currentStreamName } - if (subLogSelectedTimeRange.get().includes('last')) { + if (subLogSelectedTimeRange.get().state==='fixed') { const now = dayjs(); const timeDiff = subLogQuery.get().endTime.getTime() - subLogQuery.get().startTime.getTime(); LogQuery ={