Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.

Commit ae476dd

Browse files
committed
JSON view default
1 parent 2fb5d43 commit ae476dd

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import { Box, Stack } from '@mantine/core';
2+
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
3+
import classes from '../../Stream/styles/JSONView.module.css';
4+
import EmptyBox from '@/components/Empty';
5+
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
6+
import {
7+
PRIMARY_HEADER_HEIGHT,
8+
STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT,
9+
STREAM_SECONDARY_TOOLBAR_HRIGHT,
10+
} from '@/constants/theme';
11+
import { Log } from '@/@types/parseable/api/query';
12+
import _ from 'lodash';
13+
import { IconCheck, IconCopy, IconDotsVertical } from '@tabler/icons-react';
14+
import { copyTextToClipboard } from '@/utils';
15+
import timeRangeUtils from '@/utils/timeRangeUtils';
16+
import { useCorrelationStore } from '../providers/CorrelationProvider';
17+
import { ErrorView, LoadingView } from '@/pages/Stream/Views/Explore/LoadingViews';
18+
import { formatLogTs, isJqSearch } from '@/pages/Stream/providers/LogsProvider';
19+
20+
type ContextMenuState = {
21+
visible: boolean;
22+
x: number;
23+
y: number;
24+
row: Log | null;
25+
};
26+
27+
// const { setInstantSearchValue, applyInstantSearch, applyJqSearch } = correlationStoreReducers;
28+
29+
const Item = (props: { header: string | null; value: string; highlight: boolean }) => {
30+
return (
31+
<span className={classes.itemContainer}>
32+
<span style={{ background: props.highlight ? 'yellow' : 'transparent' }} className={classes.itemHeader}>
33+
{props.header}: {props.value}
34+
</span>
35+
</span>
36+
);
37+
};
38+
39+
export const CopyIcon = (props: { value: Log | string }) => {
40+
const copyIconRef = useRef<HTMLDivElement>(null);
41+
const copiedIconRef = useRef<HTMLDivElement>(null);
42+
43+
const onCopy = async (e: React.MouseEvent<HTMLDivElement>) => {
44+
e.stopPropagation();
45+
if (copyIconRef.current && copiedIconRef.current) {
46+
copyIconRef.current.style.display = 'none';
47+
copiedIconRef.current.style.display = 'flex';
48+
}
49+
await copyTextToClipboard(props.value);
50+
setTimeout(() => {
51+
if (copyIconRef.current && copiedIconRef.current) {
52+
copiedIconRef.current.style.display = 'none';
53+
copyIconRef.current.style.display = 'flex';
54+
}
55+
}, 1500);
56+
};
57+
58+
return (
59+
<Stack style={{ alignItems: 'center', justifyContent: 'center', marginLeft: 2 }} className={classes.toggleIcon}>
60+
<Box ref={copyIconRef} style={{ display: 'flex', height: 'auto' }} onClick={onCopy} className={classes.copyIcon}>
61+
<IconCopy stroke={1.2} size={'0.8rem'} />
62+
</Box>
63+
<Box ref={copiedIconRef} style={{ display: 'none', color: 'green' }}>
64+
<IconCheck stroke={1.2} size={'0.8rem'} />
65+
</Box>
66+
</Stack>
67+
);
68+
};
69+
70+
const localTz = timeRangeUtils.getLocalTimezone();
71+
72+
const Row = (props: {
73+
log: Log;
74+
searchValue: string;
75+
disableHighlight: boolean;
76+
isRowHighlighted: boolean;
77+
showEllipses: boolean;
78+
setContextMenu: any;
79+
shouldHighlight: (header: string | null, val: number | string | Date | null) => boolean;
80+
}) => {
81+
const [isSecureHTTPContext] = useAppStore((store) => store.isSecureHTTPContext);
82+
const { log, disableHighlight, shouldHighlight, isRowHighlighted, showEllipses, setContextMenu } = props;
83+
84+
return (
85+
<Stack
86+
style={{ flexDirection: 'row', background: isRowHighlighted ? '#E8EDFE' : 'white' }}
87+
className={classes.rowContainer}
88+
gap={0}>
89+
{showEllipses && (
90+
<div
91+
className={classes.actionIconContainer}
92+
onClick={(event) => {
93+
event.stopPropagation();
94+
setContextMenu({
95+
visible: true,
96+
x: event.pageX,
97+
y: event.pageY,
98+
row: log,
99+
});
100+
}}>
101+
<IconDotsVertical stroke={1.2} size={'0.8rem'} color="#545beb" />
102+
</div>
103+
)}
104+
<span>
105+
{_.isObject(log) ? (
106+
_.map(log, (value, key) => {
107+
//skiping fields with empty strings
108+
if (!_.toString(value)) return;
109+
const fieldTypeMap = {
110+
datetime: 'text',
111+
host: 'text',
112+
id: 'text',
113+
method: 'text',
114+
p_metadata: 'text',
115+
p_tags: 'text',
116+
p_timestamp: 'timestamp',
117+
referrer: 'text',
118+
status: 'number',
119+
'user-identifier': 'text',
120+
};
121+
const isTimestamp = _.get(fieldTypeMap, key, null) === 'timestamp';
122+
const sanitizedValue = isTimestamp ? `${formatLogTs(_.toString(value))} (${localTz})` : _.toString(value);
123+
return (
124+
<Item
125+
header={key}
126+
key={key}
127+
value={sanitizedValue}
128+
highlight={disableHighlight ? false : shouldHighlight(key, value)}
129+
/>
130+
);
131+
})
132+
) : (
133+
<Item
134+
header={null}
135+
value={_.toString(log)}
136+
highlight={disableHighlight ? false : shouldHighlight(null, _.toString(log))}
137+
/>
138+
)}
139+
</span>
140+
{isSecureHTTPContext ? <CopyIcon value={log} /> : null}
141+
</Stack>
142+
);
143+
};
144+
145+
const JsonRows = (props: { isSearching: boolean; setContextMenu: any }) => {
146+
const [{ pageData, instantSearchValue }] = useCorrelationStore((store) => store.tableOpts);
147+
const disableHighlight = props.isSearching || _.isEmpty(instantSearchValue) || isJqSearch(instantSearchValue);
148+
149+
const shouldHighlight = useCallback(
150+
(header: string | null, val: number | string | Date | null) => {
151+
return String(val).includes(instantSearchValue) || String(header).includes(instantSearchValue);
152+
},
153+
[instantSearchValue],
154+
);
155+
156+
return (
157+
<Stack gap={0} style={{ flex: 1 }}>
158+
{_.map(pageData, (d, index) => (
159+
<Row
160+
log={d}
161+
key={index}
162+
searchValue={instantSearchValue}
163+
disableHighlight={disableHighlight}
164+
shouldHighlight={shouldHighlight}
165+
isRowHighlighted={false}
166+
showEllipses={false}
167+
setContextMenu={props.setContextMenu}
168+
/>
169+
))}
170+
</Stack>
171+
);
172+
};
173+
174+
// const Toolbar = ({
175+
// isSearching,
176+
// setSearching,
177+
// }: {
178+
// isSearching: boolean;
179+
// setSearching: React.Dispatch<React.SetStateAction<boolean>>;
180+
// }) => {
181+
// const [localSearchValue, setLocalSearchValue] = useState<string>('');
182+
// const searchInputRef = useRef<HTMLInputElement>(null);
183+
184+
// const [searchValue, setCorrelationData] = useCorrelationStore((store) => store.tableOpts.instantSearchValue);
185+
// const [{ rawData, filteredData }] = useCorrelationStore((store) => store.data);
186+
187+
// const debouncedSearch = useCallback(
188+
// _.debounce(async (val: string) => {
189+
// if (val.trim() === '') {
190+
// setCorrelationData((store) => setInstantSearchValue(store, ''));
191+
// setCorrelationData(applyInstantSearch);
192+
// } else {
193+
// const isJq = isJqSearch(val);
194+
// if (isJq) {
195+
// const jqResult = await jqSearch(rawData, val);
196+
// setCorrelationData((store) => applyJqSearch(store, jqResult));
197+
// } else {
198+
// setCorrelationData(applyInstantSearch);
199+
// }
200+
// }
201+
// setSearching(false);
202+
// }, 500),
203+
// [rawData],
204+
// );
205+
206+
// const handleSearch = useCallback(() => {
207+
// if (localSearchValue.trim()) {
208+
// setSearching(true);
209+
// setCorrelationData((store) => setInstantSearchValue(store, localSearchValue));
210+
// debouncedSearch(localSearchValue);
211+
// }
212+
// }, [localSearchValue, debouncedSearch, setSearching]);
213+
214+
// const handleInputChange = useCallback(
215+
// (e: React.ChangeEvent<HTMLInputElement>) => {
216+
// const value = e.target.value;
217+
// setLocalSearchValue(value);
218+
// if (value.trim() === '') {
219+
// debouncedSearch(value);
220+
// }
221+
// },
222+
// [debouncedSearch],
223+
// );
224+
225+
// useHotkeys([['mod+K', () => searchInputRef.current?.focus()]]);
226+
227+
// const handleKeyDown = useCallback(
228+
// (e: React.KeyboardEvent<HTMLInputElement>) => {
229+
// if (e.key === 'Enter' && !isSearching && localSearchValue.trim()) {
230+
// handleSearch();
231+
// }
232+
// },
233+
// [isSearching, localSearchValue],
234+
// );
235+
236+
// if (_.isEmpty(rawData)) return null;
237+
238+
// const inputStyles = {
239+
// '--input-left-section-width': '2rem',
240+
// '--input-right-section-width': '6rem',
241+
// width: '100%',
242+
// } as React.CSSProperties;
243+
244+
// return (
245+
// <div className={classes.headerWrapper}>
246+
// <TextInput
247+
// leftSection={isSearching ? <Loader size="sm" /> : <IconSearch stroke={2.5} size="0.9rem" />}
248+
// placeholder="Search loaded data with text or jq. For jq input try `jq .[]`"
249+
// value={localSearchValue}
250+
// onChange={handleInputChange}
251+
// onKeyDown={handleKeyDown}
252+
// ref={searchInputRef}
253+
// classNames={{ input: classes.inputField }}
254+
// style={inputStyles}
255+
// rightSection={
256+
// searchValue && !isSearching ? (
257+
// <Text style={{ fontSize: '0.7rem', textAlign: 'end' }} lineClamp={1}>
258+
// {filteredData.length} Matches
259+
// </Text>
260+
// ) : null
261+
// }
262+
// />
263+
// <Button
264+
// onClick={handleSearch}
265+
// disabled={!localSearchValue.trim() || isSearching}
266+
// style={{ width: '10%' }}
267+
// leftSection={<IconSearch stroke={2.5} size="0.9rem" />}>
268+
// Search
269+
// </Button>
270+
// </div>
271+
// );
272+
// };
273+
274+
const TableContainer = (props: { children: ReactNode }) => {
275+
return <Box className={classes.container}>{props.children}</Box>;
276+
};
277+
278+
const CorrleationJSONView = (props: { errorMessage: string | null; hasNoData: boolean; showTable: boolean }) => {
279+
const [maximized] = useAppStore((store) => store.maximized);
280+
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
281+
visible: false,
282+
x: 0,
283+
y: 0,
284+
row: null,
285+
});
286+
287+
const contextMenuRef = useRef<HTMLDivElement>(null);
288+
const { errorMessage, hasNoData, showTable } = props;
289+
const [isSearching] = useState(false);
290+
const primaryHeaderHeight = !maximized
291+
? PRIMARY_HEADER_HEIGHT + STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT + STREAM_SECONDARY_TOOLBAR_HRIGHT
292+
: 0;
293+
294+
useEffect(() => {
295+
const handleClickOutside = (event: MouseEvent) => {
296+
if (contextMenuRef.current && !contextMenuRef.current.contains(event.target as Node)) {
297+
closeContextMenu();
298+
}
299+
};
300+
301+
if (contextMenu.visible) {
302+
document.addEventListener('mousedown', handleClickOutside);
303+
}
304+
305+
return () => {
306+
document.removeEventListener('mousedown', handleClickOutside);
307+
};
308+
}, [contextMenu.visible]);
309+
310+
const closeContextMenu = () => setContextMenu({ visible: false, x: 0, y: 0, row: null });
311+
312+
return (
313+
<TableContainer>
314+
{/* <Toolbar isSearching={isSearching} setSearching={setSearching} /> */}
315+
{!errorMessage ? (
316+
showTable ? (
317+
<Box className={classes.innerContainer} style={{ maxHeight: `calc(100vh - ${primaryHeaderHeight}px )` }}>
318+
<Box
319+
className={classes.innerContainer}
320+
style={{ display: 'flex', flexDirection: 'row', maxHeight: `calc(100vh - ${primaryHeaderHeight}px )` }}>
321+
<Stack gap={0} style={{ width: '100%' }}>
322+
<Stack style={{ overflowY: 'scroll' }}>
323+
<JsonRows isSearching={isSearching} setContextMenu={setContextMenu} />
324+
</Stack>
325+
</Stack>
326+
</Box>
327+
{contextMenu.visible && (
328+
<div
329+
ref={contextMenuRef}
330+
style={{
331+
top: contextMenu.y,
332+
left: contextMenu.x,
333+
}}
334+
className={classes.contextMenuContainer}
335+
onClick={closeContextMenu}></div>
336+
)}
337+
</Box>
338+
) : hasNoData ? (
339+
<>
340+
<EmptyBox message="No Matching Rows" />
341+
</>
342+
) : (
343+
<LoadingView />
344+
)
345+
) : (
346+
<ErrorView message={errorMessage || 'Failed to query logs'} />
347+
)}
348+
</TableContainer>
349+
);
350+
};
351+
352+
export default CorrleationJSONView;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Button, rem } from '@mantine/core';
2+
import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider';
3+
import { useCallback } from 'react';
4+
import { IconTable } from '@tabler/icons-react';
5+
import classes from '../styles/Correlation.module.css';
6+
7+
const { onToggleView } = correlationStoreReducers;
8+
9+
const ViewToggle = () => {
10+
const [viewMode, setCorrelationStore] = useCorrelationStore((store) => store.viewMode);
11+
const iconProps = {
12+
style: { width: rem(20), height: rem(20), display: 'block' },
13+
stroke: 1.8,
14+
};
15+
const onToggle = useCallback(() => {
16+
setCorrelationStore((store) => onToggleView(store, viewMode === 'table' ? 'json' : 'table'));
17+
}, [viewMode]);
18+
19+
const isActive = viewMode === 'table';
20+
return (
21+
<Button
22+
className={classes.savedFiltersBtn}
23+
h="100%"
24+
style={{
25+
backgroundColor: isActive ? '#535BEB' : 'white',
26+
color: isActive ? 'white' : 'black',
27+
}}
28+
onClick={onToggle}
29+
leftSection={<IconTable {...iconProps} />}>
30+
Table View
31+
</Button>
32+
);
33+
};
34+
35+
export default ViewToggle;

0 commit comments

Comments
 (0)