fix: Logs issues are fixed (#1727)

* feat: logs is updated
* chore: width:100% is removed
* chore: position of filter is updated
* chore: min time and max time are now tracked from global state


Co-authored-by: Pranay Prateek <pranay@signoz.io>
This commit is contained in:
Palash Gupta 2022-11-23 13:42:36 +05:30 committed by GitHub
parent 1273bb5865
commit 4c0d573760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 395 additions and 444 deletions

View File

@ -5,5 +5,4 @@ export const Container = styled.div`
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 0.5rem;
`; `;

View File

@ -1,26 +0,0 @@
import React from 'react';
interface OptionIconProps {
isDarkMode: boolean;
}
function OptionIcon({ isDarkMode }: OptionIconProps): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="1rem"
height="1rem"
viewBox="0 0 52 52"
enableBackground="new 0 0 52 52"
fill={isDarkMode ? '#eee' : '#222'}
>
<path
d="M20,44c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6S20,47.3,20,44z M20,26c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6
S20,29.3,20,26z M20,8c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6S20,11.3,20,8z"
/>
</svg>
);
}
export default OptionIcon;

View File

@ -0,0 +1,26 @@
export const TIME_PICKER_OPTIONS = [
{
value: 5,
label: '5m',
},
{
value: 15,
label: '15m',
},
{
value: 30,
label: '30m',
},
{
value: 60,
label: '1hr',
},
{
value: 360,
label: '6hrs',
},
{
value: 720,
label: '12hrs',
},
];

View File

@ -1,7 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { green } from '@ant-design/colors'; import { green } from '@ant-design/colors';
import { PauseOutlined, PlayCircleOutlined } from '@ant-design/icons'; import {
import { Button, Popover, Row, Select } from 'antd'; MoreOutlined,
PauseOutlined,
PlayCircleOutlined,
} from '@ant-design/icons';
import { Button, Popover, Select, Space } from 'antd';
import { LiveTail } from 'api/logs/livetail'; import { LiveTail } from 'api/logs/livetail';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
@ -18,38 +21,11 @@ import { TLogsLiveTailState } from 'types/api/logs/liveTail';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import OptionIcon from './OptionIcon'; import { TIME_PICKER_OPTIONS } from './config';
import { TimePickerCard, TimePickerSelect } from './styles'; import { StopContainer, TimePickerCard, TimePickerSelect } from './styles';
const { Option } = Select; const { Option } = Select;
const TIME_PICKER_OPTIONS = [
{
value: 5,
label: '5m',
},
{
value: 15,
label: '15m',
},
{
value: 30,
label: '30m',
},
{
value: 60,
label: '1hr',
},
{
value: 360,
label: '6hrs',
},
{
value: 720,
label: '12hrs',
},
];
function LogLiveTail(): JSX.Element { function LogLiveTail(): JSX.Element {
const { const {
liveTail, liveTail,
@ -75,14 +51,12 @@ function LogLiveTail(): JSX.Element {
type: PUSH_LIVE_TAIL_EVENT, type: PUSH_LIVE_TAIL_EVENT,
payload: batchedEventsRef.current.reverse(), payload: batchedEventsRef.current.reverse(),
}); });
// console.log('DISPATCH', batchedEventsRef.current.length);
batchedEventsRef.current = []; batchedEventsRef.current = [];
}, 1500), }, 1500),
[], [],
); );
const batchLiveLog = (e: { data: string }): void => { const batchLiveLog = (e: { data: string }): void => {
// console.log('EVENT BATCHED');
batchedEventsRef.current.push(JSON.parse(e.data as string) as never); batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
pushLiveLog(); pushLiveLog();
}; };
@ -123,6 +97,7 @@ function LogLiveTail(): JSX.Element {
if (liveTail === 'STOPPED') { if (liveTail === 'STOPPED') {
liveTailSourceRef.current = null; liveTailSourceRef.current = null;
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [liveTail]); }, [liveTail]);
const handleLiveTailStart = (): void => { const handleLiveTailStart = (): void => {
@ -155,47 +130,39 @@ function LogLiveTail(): JSX.Element {
), ),
[dispatch, liveTail, liveTailStartRange], [dispatch, liveTail, liveTailStartRange],
); );
return ( return (
<TimePickerCard> <TimePickerCard>
<Row <Space size={0} align="center">
style={{ gap: '0.5rem', alignItems: 'center', justifyContent: 'center' }} {liveTail === 'PLAYING' ? (
> <Button
<div> type="primary"
{liveTail === 'PLAYING' ? ( onClick={(): void => handleLiveTail('PAUSED')}
<Button title="Pause live tail"
type="primary" style={{ background: green[6] }}
onClick={(): void => handleLiveTail('PAUSED')} >
title="Pause live tail" <span>Pause</span>
style={{ background: green[6] }} <PauseOutlined />
> </Button>
Pause <PauseOutlined /> ) : (
</Button> <Button
) : ( type="primary"
<Button onClick={handleLiveTailStart}
type="primary" title="Start live tail"
onClick={handleLiveTailStart} >
title="Start live tail" Go Live <PlayCircleOutlined />
> </Button>
Go Live <PlayCircleOutlined /> )}
</Button>
)} {liveTail !== 'STOPPED' && (
{liveTail !== 'STOPPED' && ( <Button
<Button type="dashed"
type="dashed" onClick={(): void => handleLiveTail('STOPPED')}
onClick={(): void => handleLiveTail('STOPPED')} title="Exit live tail"
title="Exit live tail" >
> <StopContainer isDarkMode={isDarkMode} />
<div </Button>
style={{ )}
height: '0.8rem',
width: '0.8rem',
background: isDarkMode ? '#eee' : '#222',
borderRadius: '0.1rem',
}}
/>
</Button>
)}
</div>
<Popover <Popover
placement="bottomRight" placement="bottomRight"
@ -203,18 +170,9 @@ function LogLiveTail(): JSX.Element {
trigger="click" trigger="click"
content={OptionsPopOverContent} content={OptionsPopOverContent}
> >
<span <MoreOutlined style={{ fontSize: 24 }} />
style={{
padding: '0.3rem 0.4rem 0.3rem 0',
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
}}
>
<OptionIcon isDarkMode={isDarkMode} />
</span>
</Popover> </Popover>
</Row> </Space>
</TimePickerCard> </TimePickerCard>
); );
} }

View File

@ -3,6 +3,7 @@ import styled from 'styled-components';
export const TimePickerCard = styled(Card)` export const TimePickerCard = styled(Card)`
.ant-card-body { .ant-card-body {
display: flex;
padding: 0; padding: 0;
} }
`; `;
@ -10,3 +11,15 @@ export const TimePickerCard = styled(Card)`
export const TimePickerSelect = styled(Select)` export const TimePickerSelect = styled(Select)`
min-width: 100px; min-width: 100px;
`; `;
interface Props {
isDarkMode: boolean;
}
export const StopContainer = styled.div<Props>`
height: 0.8rem;
width: 0.8rem;
border-radius: 0.1rem;
background-color: ${({ isDarkMode }): string =>
isDarkMode ? '#fff' : '#000'};
`;

View File

@ -1,66 +0,0 @@
import { Divider, Row } from 'antd';
import LogControls from 'container/LogControls';
import LogDetailedView from 'container/LogDetailedView';
import LogLiveTail from 'container/LogLiveTail';
import LogsAggregate from 'container/LogsAggregate';
import LogsFilters from 'container/LogsFilters';
import SearchFilter from 'container/LogsSearchFilter';
import LogsTable from 'container/LogsTable';
import useUrlQuery from 'hooks/useUrlQuery';
import React, { memo, useEffect } from 'react';
import { connect, useDispatch } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetLogsFields } from 'store/actions/logs/getFields';
import AppActions from 'types/actions';
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
function Logs({ getLogsFields }: LogsProps): JSX.Element {
const urlQuery = useUrlQuery();
const dispatch = useDispatch();
useEffect(() => {
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: urlQuery.get('q'),
});
}, [dispatch, urlQuery]);
useEffect(() => {
getLogsFields();
}, [getLogsFields]);
return (
<div style={{ position: 'relative' }}>
<Row style={{ justifyContent: 'center', alignItems: 'center' }}>
<SearchFilter />
<Divider type="vertical" style={{ height: '2rem' }} />
<LogLiveTail />
</Row>
<LogsAggregate />
<LogControls />
<Divider style={{ margin: 0 }} />
<Row gutter={20} style={{ flexWrap: 'nowrap' }}>
<LogsFilters />
<Divider type="vertical" style={{ height: '100%', margin: 0 }} />
<LogsTable />
</Row>
<LogDetailedView />
</div>
);
}
type LogsProps = DispatchProps;
interface DispatchProps {
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(Logs));

View File

@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { blue } from '@ant-design/colors'; import { blue } from '@ant-design/colors';
import Graph from 'components/Graph'; import Graph from 'components/Graph';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
@ -16,9 +15,6 @@ import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles'; import { Container } from './styles';
interface LogsAggregateProps {
getLogsAggregate: (arg0: Parameters<typeof getLogsAggregate>[0]) => void;
}
function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
const { const {
searchFilter: { queryString }, searchFilter: { queryString },
@ -42,18 +38,18 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
clearInterval(reFetchIntervalRef.current); clearInterval(reFetchIntervalRef.current);
} }
reFetchIntervalRef.current = null; reFetchIntervalRef.current = null;
getLogsAggregate({ // getLogsAggregate({
timestampStart: minTime, // timestampStart: minTime,
timestampEnd: maxTime, // timestampEnd: maxTime,
step: getStep({ // step: getStep({
start: minTime, // start: minTime,
end: maxTime, // end: maxTime,
inputFormat: 'ns', // inputFormat: 'ns',
}), // }),
q: queryString, // q: queryString,
...(idStart ? { idGt: idStart } : {}), // ...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}), // ...(idEnd ? { idLt: idEnd } : {}),
}); // });
break; break;
} }
@ -89,18 +85,9 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
break; break;
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getLogsAggregate, maxTime, minTime, liveTail]); }, [getLogsAggregate, maxTime, minTime, liveTail]);
const data = {
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
data: logsAggregate.map((s) => s.value),
backgroundColor: blue[4],
},
],
};
return ( return (
<Container> <Container>
{isLoadingAggregate ? ( {isLoadingAggregate ? (
@ -108,7 +95,15 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
) : ( ) : (
<Graph <Graph
name="usage" name="usage"
data={data} data={{
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
data: logsAggregate.map((s) => s.value),
backgroundColor: blue[4],
},
],
}}
type="bar" type="bar"
containerHeight="100%" containerHeight="100%"
animate={false} animate={false}
@ -118,6 +113,10 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
); );
} }
interface LogsAggregateProps {
getLogsAggregate: (arg0: Parameters<typeof getLogsAggregate>[0]) => void;
}
interface DispatchProps { interface DispatchProps {
getLogsAggregate: ( getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0], props: Parameters<typeof getLogsAggregate>[0],

View File

@ -21,9 +21,6 @@ import { CategoryContainer, Container, FieldContainer } from './styles';
const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id']; const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
interface LogsFiltersProps {
getLogsFields: () => void;
}
function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element { function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
const { const {
fields: { interesting, selected }, fields: { interesting, selected },
@ -150,4 +147,6 @@ const mapDispatchToProps = (
getLogsFields: bindActionCreators(GetLogsFields, dispatch), getLogsFields: bindActionCreators(GetLogsFields, dispatch),
}); });
type LogsFiltersProps = DispatchProps;
export default connect(null, mapDispatchToProps)(memo(LogsFilters)); export default connect(null, mapDispatchToProps)(memo(LogsFilters));

View File

@ -4,7 +4,7 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
/* eslint-disable react/no-array-index-key */ /* eslint-disable react/no-array-index-key */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined, CloseSquareOutlined } from '@ant-design/icons';
import { Button, Input, Select } from 'antd'; import { Button, Input, Select } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading'; import CategoryHeading from 'components/Logs/CategoryHeading';
import { import {
@ -19,12 +19,46 @@ import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { SearchFieldsProps } from '..';
import FieldKey from '../FieldKey'; import FieldKey from '../FieldKey';
import { QueryConditionContainer, QueryFieldContainer } from '../styles'; import { QueryConditionContainer, QueryFieldContainer } from '../styles';
import { createParsedQueryStructure } from '../utils'; import { createParsedQueryStructure } from '../utils';
import { Container, QueryWrapper } from './styles';
import { hashCode, parseQuery } from './utils'; import { hashCode, parseQuery } from './utils';
const { Option } = Select; const { Option } = Select;
function QueryConditionField({
query,
queryIndex,
onUpdate,
style,
}: QueryConditionFieldProps): JSX.Element {
return (
<QueryConditionContainer style={{ ...style }}>
<Select
defaultValue={
(query as any).value &&
(((query as any)?.value as any) as string).toUpperCase()
}
onChange={(e): void => {
onUpdate({ ...query, value: e }, queryIndex);
}}
>
{Object.values(ConditionalOperators).map((cond) => (
<Option key={cond} value={cond} label={cond}>
{cond}
</Option>
))}
</Select>
</QueryConditionContainer>
);
}
QueryConditionField.defaultProps = {
style: undefined,
};
interface QueryFieldProps { interface QueryFieldProps {
query: Query; query: Query;
queryIndex: number; queryIndex: number;
@ -140,41 +174,15 @@ interface QueryConditionFieldProps {
query: { value: string | string[]; type: string }[]; query: { value: string | string[]; type: string }[];
queryIndex: number; queryIndex: number;
onUpdate: (arg0: unknown, arg1: number) => void; onUpdate: (arg0: unknown, arg1: number) => void;
} style?: React.CSSProperties;
function QueryConditionField({
query,
queryIndex,
onUpdate,
}: QueryConditionFieldProps): JSX.Element {
return (
<QueryConditionContainer>
<Select
defaultValue={
(query as any).value &&
(((query as any)?.value as any) as string).toUpperCase()
}
onChange={(e): void => {
onUpdate({ ...query, value: e }, queryIndex);
}}
style={{ width: '100%' }}
>
{Object.values(ConditionalOperators).map((cond) => (
<Option key={cond} value={cond} label={cond}>
{cond}
</Option>
))}
</Select>
</QueryConditionContainer>
);
} }
export type Query = { value: string | string[]; type: string }[]; export type Query = { value: string | string[]; type: string }[];
function QueryBuilder({ function QueryBuilder({
updateParsedQuery, updateParsedQuery,
}: { onDropDownToggleHandler,
updateParsedQuery: (arg0: unknown) => void; }: SearchFieldsProps): JSX.Element {
}): JSX.Element {
const { const {
searchFilter: { parsedQuery }, searchFilter: { parsedQuery },
} = useSelector<AppState, ILogsReducer>((store) => store.logs); } = useSelector<AppState, ILogsReducer>((store) => store.logs);
@ -233,19 +241,16 @@ function QueryBuilder({
/> />
); );
}); });
return ( return (
<div> <>
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading> <Container isMargin={generatedQueryStructure.length === 0}>
<div <CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
style={{ <CloseSquareOutlined onClick={onDropDownToggleHandler(false)} />
display: 'grid', </Container>
gridTemplateColumns: '80px 1fr',
margin: '0.5rem 0', <QueryWrapper>{QueryUI()}</QueryWrapper>
}} </>
>
{QueryUI()}
</div>
</div>
); );
} }

View File

@ -0,0 +1,17 @@
import styled from 'styled-components';
interface Props {
isMargin: boolean;
}
export const Container = styled.div<Props>`
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: ${(props): string => (props.isMargin ? '2rem' : '0')};
`;
export const QueryWrapper = styled.div`
display: grid;
grid-template-columns: 80px 1fr;
margin: 0.5rem 0px;
`;

View File

@ -1,6 +1,6 @@
import { Button } from 'antd'; import { Button } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading'; import CategoryHeading from 'components/Logs/CategoryHeading';
import { map } from 'lodash-es'; import map from 'lodash-es/map';
import React from 'react'; import React from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';

View File

@ -2,14 +2,23 @@ import React from 'react';
import QueryBuilder from './QueryBuilder/QueryBuilder'; import QueryBuilder from './QueryBuilder/QueryBuilder';
import Suggestions from './Suggestions'; import Suggestions from './Suggestions';
import { QueryFields } from './utils';
interface SearchFieldsProps { export interface SearchFieldsProps {
updateParsedQuery: () => void; updateParsedQuery: (query: QueryFields[]) => void;
onDropDownToggleHandler: (value: boolean) => VoidFunction;
} }
function SearchFields({ updateParsedQuery }: SearchFieldsProps): JSX.Element {
function SearchFields({
updateParsedQuery,
onDropDownToggleHandler,
}: SearchFieldsProps): JSX.Element {
return ( return (
<> <>
<QueryBuilder updateParsedQuery={updateParsedQuery} /> <QueryBuilder
onDropDownToggleHandler={onDropDownToggleHandler}
updateParsedQuery={updateParsedQuery}
/>
<Suggestions /> <Suggestions />
</> </>
); );

View File

@ -9,12 +9,13 @@ export const QueryFieldContainer = styled.div`
align-items: center; align-items: center;
border-radius: 0.25rem; border-radius: 0.25rem;
gap: 1rem; gap: 1rem;
width: 100%;
&:hover { &:hover {
background: ${blue[6]}; background: ${blue[6]};
} }
`; `;
export const QueryConditionContainer = styled.div` export const QueryConditionContainer = styled.div`
padding: 0.25rem 0rem; display: flex;
margin: 0.1rem 0; flex-direction: row;
`; `;

View File

@ -1,11 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */ import { Input, InputRef, Popover } from 'antd';
import { CloseSquareOutlined } from '@ant-design/icons'; import useUrlQuery from 'hooks/useUrlQuery';
import { Button, Input } from 'antd';
import useClickOutside from 'hooks/useClickOutside';
import getStep from 'lib/getStep'; import getStep from 'lib/getStep';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux'; import { connect, useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-use';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs'; import { getLogs } from 'store/actions/logs/getLogs';
@ -17,17 +14,9 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import SearchFields from './SearchFields'; import SearchFields from './SearchFields';
import { DropDownContainer } from './styles'; import { Container, DropDownContainer } from './styles';
import { useSearchParser } from './useSearchParser'; import { useSearchParser } from './useSearchParser';
const { Search } = Input;
interface SearchFilterProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
}
function SearchFilter({ function SearchFilter({
getLogs, getLogs,
getLogsAggregate, getLogsAggregate,
@ -38,6 +27,14 @@ function SearchFilter({
updateQueryString, updateQueryString,
} = useSearchParser(); } = useSearchParser();
const [showDropDown, setShowDropDown] = useState(false); const [showDropDown, setShowDropDown] = useState(false);
const searchRef = useRef<InputRef>(null);
const onDropDownToggleHandler = useCallback(
(value: boolean) => (): void => {
setShowDropDown(value);
},
[],
);
const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector< const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector<
AppState, AppState,
@ -48,117 +45,104 @@ function SearchFilter({
(state) => state.globalTime, (state) => state.globalTime,
); );
const searchComponentRef = useRef<HTMLDivElement>(null); const dispatch = useDispatch<Dispatch<AppActions>>();
useClickOutside(searchComponentRef, (e: HTMLElement) => { const handleSearch = useCallback(
// using this hack as overlay span is voilating this condition (customQuery) => {
if ( if (liveTail === 'PLAYING') {
e.nodeName === 'svg' || dispatch({
e.nodeName === 'path' || type: TOGGLE_LIVE_TAIL,
e.nodeName === 'span' || payload: 'PAUSED',
e.nodeName === 'button' });
) { setTimeout(
return; () =>
} dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
} else {
getLogs({
q: customQuery,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
if ( getLogsAggregate({
e.nodeName === 'DIV' && timestampStart: minTime,
![ timestampEnd: maxTime,
'ant-empty-image', step: getStep({
'ant-select-item', start: minTime,
'ant-col', end: maxTime,
'ant-select-item-option-content', inputFormat: 'ns',
'ant-select-item-option-active',
].find((p) => p.indexOf(e.className) !== -1) &&
!(e.ariaSelected === 'true') &&
showDropDown
) {
setShowDropDown(false);
}
});
const { search } = useLocation();
const dispatch = useDispatch();
const handleSearch = (customQuery = ''): void => {
if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}), }),
0, q: customQuery,
); });
} else { }
getLogs({ },
q: customQuery || queryString, [
limit: logLinesPerPage, dispatch,
orderBy: 'timestamp', getLogs,
order: 'desc', getLogsAggregate,
timestampStart: minTime, idEnd,
timestampEnd: maxTime, idStart,
...(idStart ? { idGt: idStart } : {}), liveTail,
...(idEnd ? { idLt: idEnd } : {}), logLinesPerPage,
}); maxTime,
minTime,
],
);
getLogsAggregate({ const urlQuery = useUrlQuery();
timestampStart: minTime, const urlQueryString = urlQuery.get('q');
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: customQuery || queryString,
});
}
setShowDropDown(false);
};
const urlQuery = useMemo(() => {
return new URLSearchParams(search);
}, [search]);
useEffect(() => { useEffect(() => {
const urlQueryString = urlQuery.get('q'); handleSearch(urlQueryString || '');
if (urlQueryString !== null) handleSearch(urlQueryString); }, [handleSearch, urlQueryString]);
}, []);
return ( return (
<div ref={searchComponentRef} style={{ flex: 1 }}> <Container>
<Search <Popover
placeholder="Search Filter" placement="bottom"
onFocus={(): void => setShowDropDown(true)} content={
value={queryString}
onChange={(e): void => {
updateQueryString(e.target.value);
}}
onSearch={handleSearch}
/>
<div style={{ position: 'relative' }}>
{showDropDown && (
<DropDownContainer> <DropDownContainer>
<Button <SearchFields
type="text" onDropDownToggleHandler={onDropDownToggleHandler}
onClick={(): void => setShowDropDown(false)} updateParsedQuery={updateParsedQuery as never}
style={{ />
position: 'absolute',
top: 0,
right: 0,
}}
>
<CloseSquareOutlined />
</Button>
<SearchFields updateParsedQuery={updateParsedQuery as never} />
</DropDownContainer> </DropDownContainer>
)} }
</div> trigger="click"
</div> overlayInnerStyle={{
width: `${searchRef?.current?.input?.offsetWidth || 0}px`,
}}
visible={showDropDown}
destroyTooltipOnHide
onVisibleChange={(value): void => {
onDropDownToggleHandler(value)();
}}
>
<Input.Search
ref={searchRef}
placeholder="Search Filter"
value={queryString}
onChange={(e): void => {
updateQueryString(e.target.value);
}}
allowClear
onSearch={handleSearch}
/>
</Popover>
</Container>
); );
} }
interface DispatchProps { interface DispatchProps {
getLogs: ( getLogs: (
props: Parameters<typeof getLogs>[0], props: Parameters<typeof getLogs>[0],
@ -168,6 +152,8 @@ interface DispatchProps {
) => (dispatch: Dispatch<AppActions>) => void; ) => (dispatch: Dispatch<AppActions>) => void;
} }
type SearchFilterProps = DispatchProps;
const mapDispatchToProps = ( const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>, dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({ ): DispatchProps => ({
@ -175,4 +161,4 @@ const mapDispatchToProps = (
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch), getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
}); });
export default connect(null, mapDispatchToProps)(memo(SearchFilter)); export default connect(null, mapDispatchToProps)(SearchFilter);

View File

@ -2,11 +2,13 @@ import { Card } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
export const DropDownContainer = styled(Card)` export const DropDownContainer = styled(Card)`
top: 0.5rem;
position: absolute;
width: 100%;
z-index: 1;
.ant-card-body { .ant-card-body {
padding: 0.8rem; width: 100%;
} }
`; `;
export const Container = styled.div`
width: 100%;
flex: 1;
position: relative;
`;

View File

@ -23,10 +23,12 @@ export function useSearchParser(): {
const updateQueryString = useCallback( const updateQueryString = useCallback(
(updatedQueryString) => { (updatedQueryString) => {
history.push({ if (updatedQueryString) {
pathname: history.location.pathname, history.push({
search: updatedQueryString ? `?q=${updatedQueryString}` : '', pathname: history.location.pathname,
}); search: updatedQueryString ? `?q=${updatedQueryString}` : '',
});
}
dispatch({ dispatch({
type: SET_SEARCH_QUERY_STRING, type: SET_SEARCH_QUERY_STRING,

View File

@ -3,48 +3,18 @@ import { Typography } from 'antd';
import LogItem from 'components/Logs/LogItem'; import LogItem from 'components/Logs/LogItem';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { map } from 'lodash-es'; import { map } from 'lodash-es';
import React, { memo, useEffect } from 'react'; import React, { memo } from 'react';
import { connect, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import { Container, Heading } from './styles'; import { Container, Heading } from './styles';
function LogsTable({ getLogs }: LogsTableProps): JSX.Element { function LogsTable(): JSX.Element {
const { const { logs, isLoading, liveTail } = useSelector<AppState, ILogsReducer>(
searchFilter: { queryString }, (state) => state.logs,
logs,
logLinesPerPage,
idEnd,
idStart,
isLoading,
liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
); );
useEffect(() => {
if (liveTail === 'STOPPED')
getLogs({
q: queryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getLogs, idEnd, idStart, logLinesPerPage, maxTime, minTime, liveTail]);
if (isLoading) { if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />; return <Spinner height={20} tip="Getting Logs" />;
} }
@ -72,20 +42,4 @@ function LogsTable({ getLogs }: LogsTableProps): JSX.Element {
); );
} }
interface DispatchProps { export default memo(LogsTable);
getLogs: (
props: Parameters<typeof getLogs>[0],
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
});
interface LogsTableProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
}
export default connect(null, mapDispatchToProps)(memo(LogsTable));

View File

@ -1,8 +1,72 @@
import Logs from 'container/Logs'; import { Divider, Row } from 'antd';
import React from 'react'; import LogControls from 'container/LogControls';
import LogDetailedView from 'container/LogDetailedView';
import LogLiveTail from 'container/LogLiveTail';
import LogsAggregate from 'container/LogsAggregate';
import LogsFilters from 'container/LogsFilters';
import LogsSearchFilter from 'container/LogsSearchFilter';
import LogsTable from 'container/LogsTable';
import useUrlQuery from 'hooks/useUrlQuery';
import React, { memo, useEffect } from 'react';
import { connect, useDispatch } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetLogsFields } from 'store/actions/logs/getFields';
import AppActions from 'types/actions';
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
function LogsHome(): JSX.Element { import SpaceContainer from './styles';
return <Logs />;
function Logs({ getLogsFields }: LogsProps): JSX.Element {
const urlQuery = useUrlQuery();
const dispatch = useDispatch();
useEffect(() => {
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: urlQuery.get('q'),
});
}, [dispatch, urlQuery]);
useEffect(() => {
getLogsFields();
}, [getLogsFields]);
return (
<>
<SpaceContainer
split={<Divider type="vertical" />}
align="center"
direction="horizontal"
>
<LogsSearchFilter />
<LogLiveTail />
</SpaceContainer>
<LogsAggregate />
<LogControls />
<Divider plain orientationMargin={1} />
<Row gutter={20} wrap={false}>
<LogsFilters />
<Divider type="vertical" />
<LogsTable />
</Row>
<LogDetailedView />
</>
);
} }
export default LogsHome; type LogsProps = DispatchProps;
interface DispatchProps {
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(Logs));

View File

@ -0,0 +1,10 @@
import { Space } from 'antd';
import styled from 'styled-components';
const SpaceContainer = styled(Space)`
.ant-space-item:nth-child(1) {
width: 100%;
}
`;
export default SpaceContainer;

View File

@ -99,15 +99,14 @@ export const LogsReducer = (
} }
case ADD_SEARCH_FIELD_QUERY_STRING: { case ADD_SEARCH_FIELD_QUERY_STRING: {
const updatedQueryString = const updatedQueryString = `${state?.searchFilter?.queryString || ''}${
state.searchFilter.queryString || state.searchFilter.queryString && state.searchFilter.queryString.length > 0
`${ ? ' and '
state.searchFilter.queryString && state.searchFilter.queryString.length > 0 : ''
? ' and ' }${action.payload}`;
: ''
}${action.payload}`;
const updatedParsedQuery = parseQuery(updatedQueryString); const updatedParsedQuery = parseQuery(updatedQueryString);
console.log({ updatedParsedQuery, updatedQueryString, action });
return { return {
...state, ...state,
searchFilter: { searchFilter: {