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

View File

@ -3,6 +3,7 @@ import styled from 'styled-components';
export const TimePickerCard = styled(Card)`
.ant-card-body {
display: flex;
padding: 0;
}
`;
@ -10,3 +11,15 @@ export const TimePickerCard = styled(Card)`
export const TimePickerSelect = styled(Select)`
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 Graph from 'components/Graph';
import Spinner from 'components/Spinner';
@ -16,9 +15,6 @@ import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles';
interface LogsAggregateProps {
getLogsAggregate: (arg0: Parameters<typeof getLogsAggregate>[0]) => void;
}
function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
const {
searchFilter: { queryString },
@ -42,18 +38,18 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
clearInterval(reFetchIntervalRef.current);
}
reFetchIntervalRef.current = null;
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
// getLogsAggregate({
// timestampStart: minTime,
// timestampEnd: maxTime,
// step: getStep({
// start: minTime,
// end: maxTime,
// inputFormat: 'ns',
// }),
// q: queryString,
// ...(idStart ? { idGt: idStart } : {}),
// ...(idEnd ? { idLt: idEnd } : {}),
// });
break;
}
@ -89,18 +85,9 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
break;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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 (
<Container>
{isLoadingAggregate ? (
@ -108,7 +95,15 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
) : (
<Graph
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"
containerHeight="100%"
animate={false}
@ -118,6 +113,10 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
);
}
interface LogsAggregateProps {
getLogsAggregate: (arg0: Parameters<typeof getLogsAggregate>[0]) => void;
}
interface DispatchProps {
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],

View File

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

View File

@ -4,7 +4,7 @@
/* eslint-disable no-param-reassign */
/* eslint-disable react/no-array-index-key */
/* 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 CategoryHeading from 'components/Logs/CategoryHeading';
import {
@ -19,12 +19,46 @@ import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import { v4 } from 'uuid';
import { SearchFieldsProps } from '..';
import FieldKey from '../FieldKey';
import { QueryConditionContainer, QueryFieldContainer } from '../styles';
import { createParsedQueryStructure } from '../utils';
import { Container, QueryWrapper } from './styles';
import { hashCode, parseQuery } from './utils';
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 {
query: Query;
queryIndex: number;
@ -140,41 +174,15 @@ interface QueryConditionFieldProps {
query: { value: string | string[]; type: string }[];
queryIndex: number;
onUpdate: (arg0: unknown, arg1: number) => void;
}
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>
);
style?: React.CSSProperties;
}
export type Query = { value: string | string[]; type: string }[];
function QueryBuilder({
updateParsedQuery,
}: {
updateParsedQuery: (arg0: unknown) => void;
}): JSX.Element {
onDropDownToggleHandler,
}: SearchFieldsProps): JSX.Element {
const {
searchFilter: { parsedQuery },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
@ -233,19 +241,16 @@ function QueryBuilder({
/>
);
});
return (
<div>
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
<div
style={{
display: 'grid',
gridTemplateColumns: '80px 1fr',
margin: '0.5rem 0',
}}
>
{QueryUI()}
</div>
</div>
<>
<Container isMargin={generatedQueryStructure.length === 0}>
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
<CloseSquareOutlined onClick={onDropDownToggleHandler(false)} />
</Container>
<QueryWrapper>{QueryUI()}</QueryWrapper>
</>
);
}

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 CategoryHeading from 'components/Logs/CategoryHeading';
import { map } from 'lodash-es';
import map from 'lodash-es/map';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';

View File

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

View File

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

View File

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

View File

@ -3,48 +3,18 @@ import { Typography } from 'antd';
import LogItem from 'components/Logs/LogItem';
import Spinner from 'components/Spinner';
import { map } from 'lodash-es';
import React, { memo, useEffect } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { Container, Heading } from './styles';
function LogsTable({ getLogs }: LogsTableProps): JSX.Element {
const {
searchFilter: { queryString },
logs,
logLinesPerPage,
idEnd,
idStart,
isLoading,
liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
function LogsTable(): JSX.Element {
const { logs, isLoading, liveTail } = useSelector<AppState, ILogsReducer>(
(state) => state.logs,
);
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) {
return <Spinner height={20} tip="Getting Logs" />;
}
@ -72,20 +42,4 @@ function LogsTable({ getLogs }: LogsTableProps): JSX.Element {
);
}
interface DispatchProps {
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));
export default memo(LogsTable);

View File

@ -1,8 +1,72 @@
import Logs from 'container/Logs';
import React from 'react';
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 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 {
return <Logs />;
import SpaceContainer from './styles';
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: {
const updatedQueryString =
state.searchFilter.queryString ||
`${
state.searchFilter.queryString && state.searchFilter.queryString.length > 0
? ' and '
: ''
}${action.payload}`;
const updatedQueryString = `${state?.searchFilter?.queryString || ''}${
state.searchFilter.queryString && state.searchFilter.queryString.length > 0
? ' and '
: ''
}${action.payload}`;
const updatedParsedQuery = parseQuery(updatedQueryString);
console.log({ updatedParsedQuery, updatedQueryString, action });
return {
...state,
searchFilter: {