Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"embla-carousel-react": "7.1.0",
"http-status-codes": "^2.2.0",
"immer": "^10.0.2",
"jq-web": "^0.5.1",
"js-cookie": "^3.0.5",
"just-compare": "^2.3.0",
"lodash": "^4.17.21",
Expand Down
166 changes: 96 additions & 70 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/constants/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const LOGS_SECONDARY_TOOLBAR_HEIGHT = 68;
export const STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT = 48;
export const STREAM_PRIMARY_TOOLBAR_HEIGHT = 26;
export const STREAM_SECONDARY_TOOLBAR_HRIGHT = 70;
export const SECONDARY_SIDEBAR_WIDTH = 50;
export const SECONDARY_SIDEBAR_WIDTH = 50;
export const JSON_VIEW_TOOLBAR_HEIGHT = 50;
8 changes: 5 additions & 3 deletions src/hooks/useQueryLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { getQueryLogs, getQueryResult } from '@/api/query';
import { StatusCodes } from 'http-status-codes';
import useMountedState from './useMountedState';
import { useCallback, useEffect, useRef } from 'react';
import { useLogsStore, logsStoreReducers, LOAD_LIMIT } from '@/pages/Stream/providers/LogsProvider';
import { useLogsStore, logsStoreReducers, LOAD_LIMIT, isJqSearch } from '@/pages/Stream/providers/LogsProvider';
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
import { useQueryResult } from './useQueryResult';
import _ from 'lodash';
import { useStreamStore } from '@/pages/Stream/providers/StreamProvider';
import { AxiosError } from 'axios';
import jqSearch from '@/utils/jqSearch';

const { setData, setTotalCount } = logsStoreReducers;

Expand Down Expand Up @@ -46,7 +47,7 @@ export const useQueryLogs = () => {
const [
{
timeRange,
tableOpts: { currentOffset },
tableOpts: { currentOffset, instantSearchValue },
custQuerySearchState,
},
setLogsStore,
Expand Down Expand Up @@ -93,7 +94,8 @@ export const useQueryLogs = () => {
const data = logsQueryRes.data;

if (logsQueryRes.status === StatusCodes.OK) {
return setLogsStore((store) => setData(store, data, schema));
const jqFilteredData = isJqSearch(instantSearchValue) ? await jqSearch(data, instantSearchValue) : [];
return setLogsStore((store) => setData(store, data, schema, jqFilteredData));
}
if (typeof data === 'string' && data.includes('Stream is not initialized yet')) {
return setLogsStore((store) => setData(store, [], schema));
Expand Down
13 changes: 13 additions & 0 deletions src/jq-web.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
declare module 'jq-web' {
interface JQWeb {
json(
records: {
[key: string]: any; // Use `any` if the values can be of any type. Replace `any` with a specific type if needed.
},
filter: string,
): Promise<any>;
}

const jq: JQWeb;
export default jq;
}
214 changes: 214 additions & 0 deletions src/pages/Stream/Views/Explore/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { FC, useCallback } from "react";
import { useLogsStore, logsStoreReducers, LOAD_LIMIT, LOG_QUERY_LIMITS } from "../../providers/LogsProvider";
import { useAppStore } from "@/layouts/MainLayout/providers/AppProvider";
import { usePagination } from "@mantine/hooks";
import { downloadDataAsCSV, downloadDataAsJson } from "@/utils/exportHelpers";
import { Box, Center, Group, Loader, Menu, Pagination, px, Stack, Tooltip } from "@mantine/core";
import _ from "lodash";
import { Text } from "@mantine/core";
import { HumanizeNumber } from "@/utils/formatBytes";
import IconButton from "@/components/Button/IconButton";
import { IconDownload, IconSelector } from "@tabler/icons-react";
import useMountedState from "@/hooks/useMountedState";
import classes from '../../styles/Footer.module.css'

const {setPageAndPageData, setCurrentPage, setCurrentOffset, makeExportData} = logsStoreReducers;

const TotalCount = (props: { totalCount: number }) => {
return (
<Tooltip label={props.totalCount}>
<Text style={{ fontSize: '0.7rem' }}>{HumanizeNumber(props.totalCount)}</Text>
</Tooltip>
);
};

const renderExportIcon = () => <IconDownload size={px('0.8rem')} stroke={1.8} />;

const TotalLogsCount = (props: { hasTableLoaded: boolean; isFetchingCount: boolean; isTableEmpty: boolean }) => {
const [{ totalCount, perPage, pageData }] = useLogsStore((store) => store.tableOpts);
const displayedCount = _.size(pageData);
const showingCount = displayedCount < perPage ? displayedCount : perPage;
if (typeof totalCount !== 'number' || typeof displayedCount !== 'number') return <Stack />;
return (
<Stack style={{ alignItems: 'center', justifyContent: 'center', flexDirection: 'row' }} gap={6}>
{props.hasTableLoaded ? (
props.isFetchingCount ? (
<Loader type="dots" />
) : (
<>
<Text style={{ fontSize: '0.7rem' }}>{`Showing ${showingCount} out of`}</Text>
<TotalCount totalCount={totalCount} />
<Text style={{ fontSize: '0.7rem' }}>records</Text>
</>
)
) : props.isTableEmpty ? null : (
<Loader type="dots" />
)}
</Stack>
);
};


const LimitControl: FC = () => {
const [opened, setOpened] = useMountedState(false);
const [perPage, setLogsStore] = useLogsStore((store) => store.tableOpts.perPage);

const toggle = () => {
setOpened(!opened);
};

const onSelect = (limit: number) => {
if (perPage !== limit) {
setLogsStore((store) => setPageAndPageData(store, 1, limit));
}
};

return (
<Box>
<Menu withArrow withinPortal shadow="md" opened={opened} onChange={setOpened}>
<Center>
<Menu.Target>
<Box onClick={toggle} className={classes.limitBtn}>
<Text className={classes.limitBtnText}>{perPage}</Text>
<IconSelector size={'0.8rem'} />
</Box>
</Menu.Target>
</Center>
<Menu.Dropdown>
{LOG_QUERY_LIMITS.map((limit) => {
return (
<Menu.Item
className={limit === perPage ? classes.limitActive : classes.limitOption}
key={limit}
onClick={() => onSelect(limit)}>
<Center>
<Text>{limit}</Text>
</Center>
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
</Box>
);
};

const Footer = (props: { loaded: boolean; isLoading: boolean; hasNoData: boolean }) => {
const [tableOpts, setLogsStore] = useLogsStore((store) => store.tableOpts);
const [filteredData] = useLogsStore((store) => store.data.filteredData);
const { totalPages, currentOffset, currentPage, perPage, headers, totalCount } = tableOpts;
const onPageChange = useCallback((page: number) => {
setLogsStore((store) => setPageAndPageData(store, page));
}, []);
const [currentStream] = useAppStore((store) => store.currentStream);
const pagination = usePagination({ total: totalPages ?? 1, initialPage: 1, onChange: onPageChange });
const onChangeOffset = useCallback(
(key: 'prev' | 'next') => {
if (key === 'prev') {
const targetOffset = currentOffset - LOAD_LIMIT;
if (currentOffset < 0) return;

if (targetOffset === 0 && currentOffset > 0) {
// hack to initiate fetch
setLogsStore((store) => setCurrentPage(store, 0));
}
setLogsStore((store) => setCurrentOffset(store, targetOffset));
} else {
const targetOffset = currentOffset + LOAD_LIMIT;
setLogsStore((store) => setCurrentOffset(store, targetOffset));
}
},
[currentOffset],
);

const exportHandler = useCallback(
(fileType: string | null) => {
const filename = `${currentStream}-logs`;
if (fileType === 'CSV') {
downloadDataAsCSV(makeExportData(filteredData, headers, 'CSV'), filename);
} else if (fileType === 'JSON') {
downloadDataAsJson(makeExportData(filteredData, headers, 'JSON'), filename);
}
},
[currentStream, filteredData, headers],
);

return (
<Stack className={classes.footerContainer} gap={0}>
<Stack w="100%" justify="center" align="flex-start">
<TotalLogsCount
hasTableLoaded={props.loaded}
isFetchingCount={props.isLoading}
isTableEmpty={props.hasNoData}
/>
</Stack>
<Stack w="100%" justify="center">
{props.loaded ? (
<Pagination.Root
total={totalPages}
value={currentPage}
onChange={(page) => {
pagination.setPage(page);
}}
size="sm">
<Group gap={5} justify="center">
<Pagination.First
onClick={() => {
currentOffset !== 0 && onChangeOffset('prev');
}}
disabled={currentOffset === 0}
/>
<Pagination.Previous />
{pagination.range.map((page, index) => {
if (page === 'dots') {
return <Pagination.Dots key={index} />;
} else {
return (
<Pagination.Control
value={page}
key={index}
active={currentPage === page}
onClick={() => {
pagination.setPage(page);
}}>
{(perPage ? page + currentOffset / perPage : page) ?? 1}
</Pagination.Control>
);
}
})}
<Pagination.Next />
<Pagination.Last
onClick={() => {
onChangeOffset('next');
}}
disabled={!(currentOffset + LOAD_LIMIT < totalCount)}
/>
</Group>
</Pagination.Root>
) : null}
</Stack>
<Stack w="100%" align="flex-end" style={{ flexDirection: 'row', justifyContent: 'flex-end' }}>
{props.loaded && (
<Menu position="top">
<Menu.Target>
<div>
<IconButton renderIcon={renderExportIcon} />
</div>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={() => exportHandler('CSV')} style={{ padding: '0.5rem 2.25rem 0.5rem 0.75rem' }}>
CSV
</Menu.Item>
<Menu.Item onClick={() => exportHandler('JSON')} style={{ padding: '0.5rem 2.25rem 0.5rem 0.75rem' }}>
JSON
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
<LimitControl />
</Stack>
</Stack>
);
};

export default Footer;
Loading