From 4d179086697b694409313bd503f0f78b955d6d98 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Mon, 11 Mar 2024 19:14:56 +0530 Subject: [PATCH 1/3] events chart refactor --- .../Header/styles/LogQuery.module.css | 2 +- src/pages/Home/index.tsx | 28 ++-- src/pages/Home/styles/Card.module.css | 4 +- src/pages/Logs/EventTimeLineGraph.tsx | 121 ++++++++++++++++-- src/pages/Logs/styles/Toolbar.module.css | 2 +- 5 files changed, 129 insertions(+), 28 deletions(-) diff --git a/src/components/Header/styles/LogQuery.module.css b/src/components/Header/styles/LogQuery.module.css index 75687302..a18b25be 100644 --- a/src/components/Header/styles/LogQuery.module.css +++ b/src/components/Header/styles/LogQuery.module.css @@ -218,7 +218,7 @@ .streamInput { border: none; padding-left: 0; - padding-right: 0; + padding-right: 30px; height: 50px; font-size: 24px; font-weight: 600; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index a6733d91..0b6820f3 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,5 +1,5 @@ import { EmptySimple } from '@/components/Empty'; -import { Text, Button, Center, Box, Group, ActionIcon, Flex } from '@mantine/core'; +import { Text, Button, Center, Box, Group, ActionIcon, Flex, Stack, Tooltip } from '@mantine/core'; import { IconChevronRight, IconExternalLink } from '@tabler/icons-react'; import { useEffect, type FC, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -9,7 +9,7 @@ import { HumanizeNumber, formatBytes } from '@/utils/formatBytes'; import { LogStreamRetention, LogStreamStat } from '@/@types/parseable/api/stream'; import { useHeaderContext } from '@/layouts/MainLayout/Context'; import cardStyles from './styles/Card.module.css'; -import homeStyles from './styles/Home.module.css' +import homeStyles from './styles/Home.module.css'; const EmptyStreamsView: FC = () => { const classes = homeStyles; @@ -36,7 +36,7 @@ const Home: FC = () => { const { container } = classes; const { methods: { streamChangeCleanup }, - state: { userSpecficStreams } + state: { userSpecficStreams }, } = useHeaderContext(); const navigate = useNavigate(); const { getStreamMetadata, metaData } = useGetStreamMetadata(); @@ -51,6 +51,7 @@ const Home: FC = () => { navigate(`/${stream}/logs`); }, []); + if (userSpecficStreams === null) return null; if ((Array.isArray(userSpecficStreams) && userSpecficStreams.length === 0) || userSpecficStreams === null) return ; @@ -69,7 +70,7 @@ export default Home; const BigNumber = (props: { label: string; value: any; color?: string }) => { return ( - + {props.label} @@ -129,20 +130,21 @@ const StreamInfo: FC = (props) => { const ingestionSize = (stats as LogStreamStat)?.ingestion?.size; const storageSize = (stats as LogStreamStat)?.storage?.size; return ( - { navigateToStream(stream); - }} - style={{ width: '100%' }}> - + }}> + {'Stream'} - - {stream} - + + + {stream} + + @@ -150,13 +152,13 @@ const StreamInfo: FC = (props) => { - + - + ); }; diff --git a/src/pages/Home/styles/Card.module.css b/src/pages/Home/styles/Card.module.css index bd13bf7b..ffe6e94e 100644 --- a/src/pages/Home/styles/Card.module.css +++ b/src/pages/Home/styles/Card.module.css @@ -6,11 +6,13 @@ .streamBox { border-radius: 0.5rem; box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.2); - flex-shrink: 0; height: 95px; padding: 1rem; cursor: pointer; width: auto; + flex-direction: row; + align-items: center; + width: 100%; } .streamBoxCol { diff --git a/src/pages/Logs/EventTimeLineGraph.tsx b/src/pages/Logs/EventTimeLineGraph.tsx index 7b487117..6bf2fa05 100644 --- a/src/pages/Logs/EventTimeLineGraph.tsx +++ b/src/pages/Logs/EventTimeLineGraph.tsx @@ -1,11 +1,12 @@ -import { Skeleton, Stack, Text } from '@mantine/core'; +import { Paper, Skeleton, Stack, Text } from '@mantine/core'; import classes from './styles/EventTimeLineGraph.module.css'; import { useQueryResult } from '@/hooks/useQueryResult'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useLogsPageContext } from './logsContextProvider'; import dayjs from 'dayjs'; -import { AreaChart } from '@mantine/charts'; +import { BarChart, ChartTooltipProps } from '@mantine/charts'; import { HumanizeNumber } from '@/utils/formatBytes'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; const generateCountQuery = (streamName: string, startTime: string, endTime: string) => { return `SELECT DATE_TRUNC('minute', p_timestamp) AS minute_range, COUNT(*) AS log_count FROM ${streamName} WHERE p_timestamp BETWEEN '${startTime}' AND '${endTime}' GROUP BY minute_range ORDER BY minute_range`; @@ -15,19 +16,80 @@ const NoDataView = () => { return ( - No new events in the last 10 minutes. + No new events in the last 30 minutes. ); }; +type GraphRecord = { + minute_range: string; + log_count: number; +}; + +const calcAverage = (data: GraphRecord[]) => { + if (!Array.isArray(data) || data.length === 0) return 0; + const total = data.reduce((acc, d) => { + return acc + d.log_count; + }, 0); + return parseInt(Math.abs(total / data.length).toFixed(0)); +}; + +const parseGraphData = (data: GraphRecord[], avg: number) => { + if (!Array.isArray(data) || data.length === 0) return []; + + const parsed = data.map((d) => { + const aboveAvgCount = d.log_count - avg; + const aboveAvgPercent = parseInt(((aboveAvgCount / avg) * 100).toFixed(2)); + + const data = { + events: d.log_count, + belowAvgCount: aboveAvgCount >= 0 ? avg : d.log_count, + aboveAvgCount: aboveAvgCount <= 0 ? 0 : aboveAvgCount, + minute: `${d.minute_range}Z`, + aboveAvgPercent, + }; + return data; + }); + return parsed; +}; + +function ChartTooltip({ payload }: ChartTooltipProps) { + if (!payload || (Array.isArray(payload) && payload.length === 0)) return null; + + const totalEvents = payload.reduce((acc, item: any) => { + return acc + item.value; + }, 0); + + const { minute, aboveAvgPercent } = payload[0]?.payload || {}; + const isAboveAvg = aboveAvgPercent > 0; + const startTime = dayjs(minute).utc(true); + const endTime = dayjs(minute).add(59, 'seconds'); + return ( + + + {`${startTime.format('HH:mm:ss A')} - ${endTime.format('HH:mm:ss A')}`} + + + Events + {totalEvents} + + + {`${ + isAboveAvg ? '+' : '' + }${aboveAvgPercent}% above avg (Last 30 mins)`} + + + ); +} + const EventTimeLineGraph = () => { const { fetchQueryMutation } = useQueryResult(); const { state: { currentStream }, } = useLogsPageContext(); - const endTime = dayjs().subtract(1, 'minute').startOf('minute'); - const startTime = endTime.subtract(10, 'minute').startOf('minute'); + const endTime = dayjs().subtract(2, 'minute').startOf('minute'); + const startTime = endTime.subtract(30, 'minute').startOf('minute'); useEffect(() => { if (!currentStream || currentStream.length === 0) return; @@ -45,10 +107,36 @@ const EventTimeLineGraph = () => { }); }, [currentStream]); - const graphData = fetchQueryMutation?.data; const isLoading = fetchQueryMutation.isLoading; + const avgEventCount = useMemo(() => calcAverage(fetchQueryMutation?.data), [fetchQueryMutation?.data]); + const graphData = useMemo(() => parseGraphData(fetchQueryMutation?.data, avgEventCount), [fetchQueryMutation?.data]); const hasData = Array.isArray(graphData) && graphData.length !== 0; + const { + state: { subLogQuery, subLogSelectedTimeRange }, + } = useHeaderContext(); + + const setTimeRange = useCallback((barValue: any) => { + const activePayload = barValue?.activePayload; + if (!Array.isArray(activePayload) || activePayload.length === 0) return; + + const samplePayload = activePayload[0]; + if (!samplePayload || typeof samplePayload !== 'object') return; + + const { minute } = samplePayload.payload || {}; + const startTime = dayjs(minute); + const endTime = dayjs(minute).add(59, 'seconds'); + subLogQuery.set((query) => { + query.startTime = startTime.toDate(); + query.endTime = endTime.toDate(); + }); + + subLogSelectedTimeRange.set((state) => { + state.state = 'custom'; + state.value = `${startTime.format('DD-MM-YY HH:mm:ss')} - ${endTime.format('DD-MM-YY HH:mm:ss')}`; + }); + }, []); + return ( { w={isLoading ? '98%' : '100%'} style={isLoading ? { marginLeft: '1.8rem', alignSelf: 'center' } : !hasData ? { marginLeft: '1rem' } : {}}> {hasData ? ( - , + position: { y: -20 }, + }} + type="stacked" valueFormatter={(value) => new Intl.NumberFormat('en-US').format(value)} withXAxis={false} withYAxis={hasData} - curveType="linear" yAxisProps={{ tickCount: 2, tickFormatter: (value) => `${HumanizeNumber(value)}` }} + referenceLines={[{ y: avgEventCount, color: 'red.5' }]} + tickLine="none" + barChartProps={{ onClick: setTimeRange }} gridAxis="xy" - withGradient={false} /> ) : ( diff --git a/src/pages/Logs/styles/Toolbar.module.css b/src/pages/Logs/styles/Toolbar.module.css index 5b8e70a4..496dd339 100644 --- a/src/pages/Logs/styles/Toolbar.module.css +++ b/src/pages/Logs/styles/Toolbar.module.css @@ -8,7 +8,7 @@ .streamInput { border: none; padding-left: 0; - padding-right: 0; + padding-right: 30; height: 50px; font-size: 24px; font-weight: 600; From cca5c9bf6ff1b45721b889a749bcd46a28ff4f1c Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 12 Mar 2024 17:33:36 +0530 Subject: [PATCH 2/3] log table cosmetic changes --- src/pages/Home/index.tsx | 2 +- src/pages/Logs/Column.tsx | 87 +++++++++++++------------ src/pages/Logs/EventTimeLineGraph.tsx | 70 ++++++++++++++------ src/pages/Logs/styles/Column.module.css | 11 +++- 4 files changed, 104 insertions(+), 66 deletions(-) diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 0b6820f3..94a42335 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -52,7 +52,7 @@ const Home: FC = () => { }, []); if (userSpecficStreams === null) return null; - if ((Array.isArray(userSpecficStreams) && userSpecficStreams.length === 0) || userSpecficStreams === null) + if ((Array.isArray(userSpecficStreams) && userSpecficStreams.length === 0)) return ; return ( diff --git a/src/pages/Logs/Column.tsx b/src/pages/Logs/Column.tsx index 02ea031c..bc99415c 100644 --- a/src/pages/Logs/Column.tsx +++ b/src/pages/Logs/Column.tsx @@ -1,17 +1,16 @@ import { Log, SortOrder } from '@/@types/parseable/api/query'; -import { Box, Checkbox, Popover, TextInput, Tooltip, UnstyledButton, px } from '@mantine/core'; +import { Box, Checkbox, Popover, ScrollArea, Stack, TextInput, Tooltip, UnstyledButton, px } from '@mantine/core'; import { type ChangeEvent, type FC, Fragment, useTransition, useRef, useCallback, useMemo } from 'react'; import { IconDotsVertical, IconFilter, IconSearch, IconSortAscending, IconSortDescending } from '@tabler/icons-react'; import useMountedState from '@/hooks/useMountedState'; import EmptyBox from '@/components/Empty'; import { Button } from '@mantine/core'; import Loading from '@/components/Loading'; -import { FixedSizeList as List } from 'react-window'; import compare from 'just-compare'; import { parseLogData } from '@/utils'; -import { useDisclosure } from '@mantine/hooks'; import { capitalizeFirstLetter } from '@/utils/capitalizeFirstLetter'; -import columnStyles from './styles/Column.module.css' +import columnStyles from './styles/Column.module.css'; +import { Text } from '@mantine/core'; type SortWidgetProps = { setSortOrder: (order: SortOrder | null) => void; @@ -77,7 +76,7 @@ const Column: FC = (props) => { if (values && search) { return values.filter((x) => { - return parseLogData(x, columnName)?.toString().includes(search); + return x?.toString().toLowerCase().includes(search.toLowerCase()); }); } @@ -91,7 +90,10 @@ const Column: FC = (props) => { const onOpen = useCallback(() => { if (!_columnValuesRef.current) { - _columnValuesRef.current = getColumnFilters(columnName); + const uniqueValues = getColumnFilters(columnName); + _columnValuesRef.current = Array.isArray(uniqueValues) + ? uniqueValues?.map((val) => parseLogData(val, columnName)) + : null; startTransition(() => { setColumnValues(_columnValuesRef.current); }); @@ -170,49 +172,48 @@ type CheckboxVirtualListProps = { setFilters: (value: string[]) => void; }; +const SLICE_OFFSET = 50; + const CheckboxVirtualList: FC = (props) => { - const { list, selectedFilters, setFilters, columnName } = props; + const { list, selectedFilters, setFilters } = props; + const classes = columnStyles; + const totalValues = list.length; + const shortList = list.slice(0, SLICE_OFFSET); + const { checkBoxStyle } = classes; + + const remainingLength = totalValues > SLICE_OFFSET ? totalValues - SLICE_OFFSET : 0; return ( - - {({ index }) => { - const label = list[index]?.toString() || ''; - - return ; - }} - + + {shortList.map((item, index) => { + const label = item?.toString() || ''; + return ( +
+ + + + + + {index + 1 === shortList.length && remainingLength > 0 && ( + {`+${remainingLength} more`} + )} +
+ ); + })} +
); }; -type CheckboxRowProps = { - label: string; - value: string; - columnName: string; -}; - -const CheckboxRow: FC = (props) => { - const { value, label, columnName } = props; - const [opened, { open, close }] = useDisclosure(false); - const classes = columnStyles; - const { checkBoxStyle } = classes; - return ( - -
- -
-
- ); -}; - export default Column; diff --git a/src/pages/Logs/EventTimeLineGraph.tsx b/src/pages/Logs/EventTimeLineGraph.tsx index 6bf2fa05..f47b3e60 100644 --- a/src/pages/Logs/EventTimeLineGraph.tsx +++ b/src/pages/Logs/EventTimeLineGraph.tsx @@ -1,13 +1,16 @@ import { Paper, Skeleton, Stack, Text } from '@mantine/core'; import classes from './styles/EventTimeLineGraph.module.css'; import { useQueryResult } from '@/hooks/useQueryResult'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useLogsPageContext } from './logsContextProvider'; -import dayjs from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { BarChart, ChartTooltipProps } from '@mantine/charts'; import { HumanizeNumber } from '@/utils/formatBytes'; import { useHeaderContext } from '@/layouts/MainLayout/Context'; +const START_RANGE = 30 +const END_RANGE = 2 + const generateCountQuery = (streamName: string, startTime: string, endTime: string) => { return `SELECT DATE_TRUNC('minute', p_timestamp) AS minute_range, COUNT(*) AS log_count FROM ${streamName} WHERE p_timestamp BETWEEN '${startTime}' AND '${endTime}' GROUP BY minute_range ORDER BY minute_range`; }; @@ -35,23 +38,46 @@ const calcAverage = (data: GraphRecord[]) => { return parseInt(Math.abs(total / data.length).toFixed(0)); }; -const parseGraphData = (data: GraphRecord[], avg: number) => { - if (!Array.isArray(data) || data.length === 0) return []; +const getAllTimestamps = (startTime: Dayjs) => { + const timestamps = []; + for (let i = 0; i < START_RANGE; i++) { + // Your code to be executed 30 times goes here + const ts = startTime.add(i + 1, 'minute') + timestamps.push(ts.toISOString().split('.')[0]+"Z") + } + return timestamps; +} - const parsed = data.map((d) => { - const aboveAvgCount = d.log_count - avg; - const aboveAvgPercent = parseInt(((aboveAvgCount / avg) * 100).toFixed(2)); +// date_trunc removes tz info +// filling data empty values where there is no rec +const parseGraphData = (data: GraphRecord[], avg: number, startTime: Dayjs) => { + if (!Array.isArray(data) || data.length === 0) return []; - const data = { - events: d.log_count, - belowAvgCount: aboveAvgCount >= 0 ? avg : d.log_count, - aboveAvgCount: aboveAvgCount <= 0 ? 0 : aboveAvgCount, - minute: `${d.minute_range}Z`, - aboveAvgPercent, - }; - return data; + const allTimestamps = getAllTimestamps(startTime) + const parsedData = allTimestamps.map((ts) => { + const countData = data.find((d) => `${d.minute_range}Z` === ts); + if (!countData || typeof countData !== 'object') { + return { + events: 0, + belowAvgCount: 0, + aboveAvgCount: 0, + minute: ts, + aboveAvgPercent: 0, + }; + } else { + const aboveAvgCount = countData.log_count - avg; + const aboveAvgPercent = parseInt(((aboveAvgCount / avg) * 100).toFixed(2)); + return { + events: countData.log_count, + belowAvgCount: aboveAvgCount >= 0 ? avg : countData.log_count, + aboveAvgCount: aboveAvgCount <= 0 ? 0 : aboveAvgCount, + minute: `${countData.minute_range}Z`, + aboveAvgPercent, + }; + } }); - return parsed; + + return parsedData; }; function ChartTooltip({ payload }: ChartTooltipProps) { @@ -83,13 +109,19 @@ function ChartTooltip({ payload }: ChartTooltipProps) { ); } +const generateTimeOpts = () => { + const endTime = dayjs().subtract(END_RANGE, 'minute').startOf('minute'); + const startTime = endTime.subtract(START_RANGE, 'minute').startOf('minute'); + return { startTime, endTime }; +}; + const EventTimeLineGraph = () => { const { fetchQueryMutation } = useQueryResult(); const { state: { currentStream }, } = useLogsPageContext(); - const endTime = dayjs().subtract(2, 'minute').startOf('minute'); - const startTime = endTime.subtract(30, 'minute').startOf('minute'); + const [timeOpts, _setTimeOpts] = useState<{ startTime: Dayjs; endTime: Dayjs }>(generateTimeOpts()); + const { endTime, startTime } = timeOpts; useEffect(() => { if (!currentStream || currentStream.length === 0) return; @@ -109,7 +141,7 @@ const EventTimeLineGraph = () => { const isLoading = fetchQueryMutation.isLoading; const avgEventCount = useMemo(() => calcAverage(fetchQueryMutation?.data), [fetchQueryMutation?.data]); - const graphData = useMemo(() => parseGraphData(fetchQueryMutation?.data, avgEventCount), [fetchQueryMutation?.data]); + const graphData = useMemo(() => parseGraphData(fetchQueryMutation?.data, avgEventCount, startTime), [fetchQueryMutation?.data]); const hasData = Array.isArray(graphData) && graphData.length !== 0; const { diff --git a/src/pages/Logs/styles/Column.module.css b/src/pages/Logs/styles/Column.module.css index 58168dda..a51caf49 100644 --- a/src/pages/Logs/styles/Column.module.css +++ b/src/pages/Logs/styles/Column.module.css @@ -37,12 +37,13 @@ } .checkBoxStyle { - height: 35px; - padding-top: 0.625rem; - padding-bottom: 0.3125rem; + padding: 0.75rem 0.5rem 0.75rem 0.5rem; + border-radius: 4px; font-weight: 400; overflow: hidden; white-space: nowrap; + width: 18rem; + text-overflow: ellipsis; } .checkBoxStyle .mantine-Checkbox-label { @@ -100,3 +101,7 @@ .sortBtnActive:hover { color: #fc466b; } + +.labell { + text-overflow: ellipsis; +} \ No newline at end of file From 770289912df7bf7791be06e39d97284f16777d19 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 12 Mar 2024 17:35:50 +0530 Subject: [PATCH 3/3] rm unwanted change --- src/pages/Logs/styles/Column.module.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/Logs/styles/Column.module.css b/src/pages/Logs/styles/Column.module.css index a51caf49..303b9ed8 100644 --- a/src/pages/Logs/styles/Column.module.css +++ b/src/pages/Logs/styles/Column.module.css @@ -101,7 +101,3 @@ .sortBtnActive:hover { color: #fc466b; } - -.labell { - text-overflow: ellipsis; -} \ No newline at end of file