From 112ec3d6fa58e4e0b68fd1a022dc4c73581299bd Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 19 Jan 2024 11:31:45 +0530 Subject: [PATCH] ISSUE-178 - added instant exports to explore tab --- src/components/Header/Dropdown.tsx | 17 ++- src/components/Header/LiveTailFilter.tsx | 8 +- src/components/Header/SecondaryHeader.tsx | 32 ----- src/components/Header/SubHeader.tsx | 153 +++++++++++++--------- src/components/Header/index.tsx | 2 - src/hooks/useExportData.ts | 59 +++++++++ src/hooks/useQueryLogs.ts | 11 +- src/layouts/MainLayout/index.tsx | 3 +- src/pages/Logs/Context.tsx | 11 ++ src/routes/elements.tsx | 14 ++ 10 files changed, 209 insertions(+), 101 deletions(-) delete mode 100644 src/components/Header/SecondaryHeader.tsx create mode 100644 src/hooks/useExportData.ts diff --git a/src/components/Header/Dropdown.tsx b/src/components/Header/Dropdown.tsx index 0dcaa733..4fc1179c 100644 --- a/src/components/Header/Dropdown.tsx +++ b/src/components/Header/Dropdown.tsx @@ -3,13 +3,26 @@ import { Select } from '@mantine/core'; type DropdownProps = { data: string[]; + placeholder?: string; + searchable?: boolean; + value?: string | null; onChange: (value: string) => void; }; const Dropdown: FC = (props) => { - const { data, onChange } = props; + const { data, onChange, placeholder = "Export", searchable = false, value = null } = props; - return + ); }; export default Dropdown; diff --git a/src/components/Header/LiveTailFilter.tsx b/src/components/Header/LiveTailFilter.tsx index ee0b7dbc..f0525e01 100644 --- a/src/components/Header/LiveTailFilter.tsx +++ b/src/components/Header/LiveTailFilter.tsx @@ -54,7 +54,13 @@ const LiveTailFilter: FC = () => { {schemaData.length > 0 && ( <> - item.name)} onChange={handleDropdownValue} /> + item.name)} + searchable + onChange={handleDropdownValue} + placeholder="Column" + value={searchField} + /> ; - -const SecondaryHeader: FC = (props) => { - return ( - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - - ); -}; - -export default SecondaryHeader; diff --git a/src/components/Header/SubHeader.tsx b/src/components/Header/SubHeader.tsx index e7ec796b..8188821e 100644 --- a/src/components/Header/SubHeader.tsx +++ b/src/components/Header/SubHeader.tsx @@ -1,4 +1,4 @@ -import { Box } from '@mantine/core'; +import { Box, Header as MantineHeader } from '@mantine/core'; import type { FC } from 'react'; import HeaderBreadcrumbs from './HeaderBreadcrumbs'; import RefreshInterval from './RefreshInterval'; @@ -10,25 +10,46 @@ import ReloadUser from './ReloadUser'; import DocsUser from './UserDocs'; import StreamingButton from './StreamingButton'; import LiveTailFilter from './LiveTailFilter'; +import Dropdown from './Dropdown'; +import { useHeaderStyles } from './styles'; +import { HEADER_HEIGHT } from '@/constants/theme'; +import { useExportData } from '@/hooks/useExportData'; + +type HeaderLayoutProps = { + children: React.ReactNode; +}; + +const HeaderLayout: FC = (props) => { + const { classes } = useHeaderStyles(); + const { container, navContainer } = classes; + + return ( + + {props.children} + + ); +}; export const StatsHeader: FC = () => { const { classes } = useLogQueryStyles(); const { container, innerContainer } = classes; return ( - - - - + + + + + + - - - - + + + + - + ); }; @@ -37,20 +58,22 @@ export const QueryHeader: FC = () => { const { container, innerContainer } = classes; return ( - - - - + + + + + + - - - - - + + + + + - + ); }; @@ -59,48 +82,54 @@ export const LiveTailHeader: FC = () => { const { container, innerContainer } = classes; return ( - - - - + + + + + + - - - - - - {/* */} - {/* */} + + + + + {/* */} + {/* */} + - + ); }; export const LogsHeader: FC = () => { const { classes } = useLogQueryStyles(); const { container, innerContainer } = classes; + const { exportLogsHandler } = useExportData(); return ( - - - - + + + + + + - - - - - - {/* */} + + + + + {/* */} - - + + + + - + ); }; @@ -124,19 +153,21 @@ export const UsersManagementHeader: FC = () => { const { container, innerContainer } = classes; return ( - - - - + + + + + + - - - - - + + + + + - + ); }; @@ -145,12 +176,14 @@ export const AllRouteHeader: FC = () => { const { container, innerContainer } = classes; return ( - - - - + + + + + + - + ); }; diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 70446cb4..ce7ae37e 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,7 +1,5 @@ import PrimaryHeader from "./PrimaryHeader"; -import SecondaryHeader from "./SecondaryHeader"; export { PrimaryHeader, - SecondaryHeader } \ No newline at end of file diff --git a/src/hooks/useExportData.ts b/src/hooks/useExportData.ts new file mode 100644 index 00000000..728ff450 --- /dev/null +++ b/src/hooks/useExportData.ts @@ -0,0 +1,59 @@ +// import { useEffect, useState } from 'react'; +import type { Log } from '@/@types/parseable/api/query'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import { useLogsPageContext } from '@/pages/Logs/Context'; + +type Data = Log[] | null; + +const downloadDataAsJson = (data: Data, filename: string) => { + if (data === null || data.length === 0) return; + + const jsonString = JSON.stringify(data, null, 2); + const blob = new Blob([jsonString], { type: 'application/json' }); + const downloadLink = document.createElement('a'); + downloadLink.href = URL.createObjectURL(blob); + downloadLink.download = `${filename}.json`; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); +}; + +const downloadDataAsCSV = (data: Data, filename: string) => { + if (data === null || data.length === 0) return; + + const csvString = data + .map((row) => + Object.values(row) + .map((value) => (value !== null ? value : '')) + .join(','), + ) + .join('\n'); + const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); + const downloadLink = document.createElement('a'); + downloadLink.href = URL.createObjectURL(blob); + downloadLink.download = `${filename}.csv`; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); +}; + +export const useExportData = () => { + const { + state: { subLogQueryData }, + } = useLogsPageContext(); + const { + state: { subLogQuery }, + } = useHeaderContext(); + const exportLogsHandler = (type: string) => { + const { rawData, filteredData: _filteredData } = subLogQueryData.get(); // filteredData - records filtered with in-page search + const query = subLogQuery.get(); + const filename = `${query.streamName}-logs`; + type === 'JSON' + ? downloadDataAsJson(rawData, filename) + : type === 'CSV' + ? downloadDataAsCSV(rawData, filename) + : null; + }; + + return { exportLogsHandler }; +}; diff --git a/src/hooks/useQueryLogs.ts b/src/hooks/useQueryLogs.ts index cfdd4aac..e32f347b 100644 --- a/src/hooks/useQueryLogs.ts +++ b/src/hooks/useQueryLogs.ts @@ -3,7 +3,7 @@ import { getQueryLogs } from '@/api/query'; import { StatusCodes } from 'http-status-codes'; import useMountedState from './useMountedState'; import { useCallback, useEffect, useMemo, useRef, useTransition } from 'react'; -import { LOG_QUERY_LIMITS } from '@/pages/Logs/Context'; +import { LOG_QUERY_LIMITS, useLogsPageContext } from '@/pages/Logs/Context'; import { parseLogData } from '@/utils'; type QueryLogs = { @@ -32,11 +32,14 @@ export const useQueryLogs = () => { }, }); const [isPending, startTransition] = useTransition(); + const { + state: { subLogQueryData }, + } = useLogsPageContext(); const data: Log[] | null = useMemo(() => { if (_dataRef.current) { const logs = _dataRef.current; - const temp = []; + const temp: Log[] = []; const { search, filters, sort } = querySearch; const searchText = search.trim().toLowerCase(); const filteredKeys = Object.keys(filters); @@ -78,6 +81,10 @@ export const useQueryLogs = () => { return res * order; }); + subLogQueryData.set((state) => { + state.filteredData = temp; + state.rawData = logs; + }); return temp; } diff --git a/src/layouts/MainLayout/index.tsx b/src/layouts/MainLayout/index.tsx index d17f0b84..6edea930 100644 --- a/src/layouts/MainLayout/index.tsx +++ b/src/layouts/MainLayout/index.tsx @@ -1,4 +1,4 @@ -import { PrimaryHeader, SecondaryHeader } from '@/components/Header'; +import { PrimaryHeader } from '@/components/Header'; import Navbar from '@/components/Navbar'; import { NAVBAR_WIDTH } from '@/constants/theme'; import { Box } from '@mantine/core'; @@ -21,7 +21,6 @@ const MainLayout: FC = () => { display: 'flex', flexDirection: 'column', }}> - diff --git a/src/pages/Logs/Context.tsx b/src/pages/Logs/Context.tsx index 1c6c0492..82cb56ac 100644 --- a/src/pages/Logs/Context.tsx +++ b/src/pages/Logs/Context.tsx @@ -19,6 +19,12 @@ interface LogsPageContextState { subLogStreamError: SubData; subViewLog: SubData; subGapTime: SubData; + subLogQueryData: SubData; +} + +type LogQueryData = { + rawData: Log[] | [], + filteredData: Log[] | [] } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -37,11 +43,16 @@ const LogsPageProvider: FC = ({ children }) => { const subLogStreamError = useSubscribeState(null); const subViewLog = useSubscribeState(null); const subGapTime = useSubscribeState(null); + const subLogQueryData = useSubscribeState({ + rawData: [], + filteredData: [], + }); const state: LogsPageContextState = { subLogStreamError, subViewLog, subGapTime, + subLogQueryData }; const methods: LogsPageContextMethods = {}; diff --git a/src/routes/elements.tsx b/src/routes/elements.tsx index 49e84243..834dedad 100644 --- a/src/routes/elements.tsx +++ b/src/routes/elements.tsx @@ -6,6 +6,14 @@ import SuspensePage from './SuspensePage'; import QueryPageProvider from '@/pages/Query/Context'; import MainLayoutPageProvider from '@/layouts/MainLayout/Context'; import MainLayout from '@/layouts/MainLayout'; +import { + ConfigHeader, + LiveTailHeader, + LogsHeader, + QueryHeader, + StatsHeader, + UsersManagementHeader, +} from '@/components/Header/SubHeader'; export const HomeElement: FC = () => ; @@ -25,6 +33,7 @@ export const LogsElement: FC = () => { return ( + @@ -37,6 +46,7 @@ export const QueryElement: FC = () => { return ( + @@ -56,6 +66,7 @@ const LiveTail = lazy(() => import('@/pages/LiveTail')); export const LiveTailElement: FC = () => { return ( + ); @@ -66,6 +77,7 @@ const Stats = lazy(() => import('@/pages/Stats')); export const StatsElement: FC = () => { return ( + ); @@ -76,6 +88,7 @@ const Config = lazy(() => import('@/pages/Config')); export const ConfigElement: FC = () => { return ( + ); @@ -86,6 +99,7 @@ const Users = lazy(() => import('@/pages/AccessManagement')); export const UsersElement: FC = () => { return ( + );