Logs pipeline editor - filter preview (#3683)

* feat: get started with Logs Filter Preview

* chore: rename PipelineFilterPreview -> PipelineFilterSummary

* chore: initial styles for pipeline filter preview

* feat: wire up logs fetching for pipeline filter preview

* feat: show empty preview if filter is empty

* feat: get logs preview table display started

* feat: use simple div + style based display for logs preview

* feat: log preview item expand action

* feat: move preview below filter and make filter last i/p in pipeline form

* feat: add duration selector for logs filter preview

* feat: add matched logs count to pipeline filter preview

* chore: reorganize preview logs list into its own file

* chore: cleanup

* chore: revert type export from useGetQueryRange.ts

* chore: get all tests passing

* chore: address review comments: import cloneDeep directly

* chore: address review comments: avoid inline handler func, return JSX.Element | null

* chore: address review comments: move preview interval selector helper into its own folder

* chore: address feedback: fix cloneDeep import

* chore: address feedback: avoid inline handler and remove eslint supression
This commit is contained in:
Raj Kamal Singh 2023-10-08 14:49:16 +05:30 committed by GitHub
parent d7d4000240
commit 9e91375632
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 398 additions and 23 deletions

View File

@ -1,3 +1,5 @@
import './styles.scss';
import { Form } from 'antd';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
@ -5,9 +7,10 @@ import isEqual from 'lodash-es/isEqual';
import { useTranslation } from 'react-i18next';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { ProcessorFormField } from '../../AddNewProcessor/config';
import { formValidationRules } from '../../config';
import { FormLabelStyle } from '../styles';
import { ProcessorFormField } from '../../../AddNewProcessor/config';
import { formValidationRules } from '../../../config';
import LogsFilterPreview from '../../../Preview/LogsFilterPreview';
import { FormLabelStyle } from '../../styles';
function TagFilterInput({
value,
@ -41,9 +44,27 @@ interface TagFilterInputProps {
placeholder: string;
}
function TagFilterInputWithLogsResultPreview({
value,
onChange,
placeholder,
}: TagFilterInputProps): JSX.Element {
return (
<>
<TagFilterInput
placeholder={placeholder}
value={value}
onChange={onChange}
/>
<div className="pipeline-filter-input-preview-container">
<LogsFilterPreview filter={value} />
</div>
</>
);
}
function FilterInput({ fieldData }: FilterInputProps): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<Form.Item
required={false}
@ -52,9 +73,11 @@ function FilterInput({ fieldData }: FilterInputProps): JSX.Element {
rules={formValidationRules}
name={fieldData.name}
>
{/* Antd form will supply value and onChange to <TagFilterInput /> here.
{/* Antd form will supply value and onChange here.
// @ts-ignore */}
<TagFilterInput placeholder={t(fieldData.placeholder)} />
<TagFilterInputWithLogsResultPreview
placeholder={t(fieldData.placeholder)}
/>
</Form.Item>
);
}

View File

@ -0,0 +1,3 @@
.pipeline-filter-input-preview-container {
margin-top: 1rem;
}

View File

@ -0,0 +1,43 @@
import './styles.scss';
import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelection/config';
import { useState } from 'react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import PreviewIntervalSelector from '../components/PreviewIntervalSelector';
import SampleLogs from '../components/SampleLogs';
function LogsFilterPreview({ filter }: LogsFilterPreviewProps): JSX.Element {
const last1HourInterval = RelativeDurationOptions[3].value;
const [previewTimeInterval, setPreviewTimeInterval] = useState(
last1HourInterval,
);
const isEmptyFilter = (filter?.items?.length || 0) < 1;
return (
<div>
<div className="logs-filter-preview-header">
<div>Filtered Logs Preview</div>
<PreviewIntervalSelector
previewFilter={filter}
value={previewTimeInterval}
onChange={setPreviewTimeInterval}
/>
</div>
<div className="logs-filter-preview-content">
{isEmptyFilter ? (
<div>Please select a filter</div>
) : (
<SampleLogs filter={filter} timeInterval={previewTimeInterval} />
)}
</div>
</div>
);
}
interface LogsFilterPreviewProps {
filter: TagFilter;
}
export default LogsFilterPreview;

View File

@ -0,0 +1,18 @@
.logs-filter-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 8px;
}
.logs-filter-preview-content {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 8rem;
overflow: hidden;
border: 1px solid rgba(253, 253, 253, 0.12);
}

View File

@ -0,0 +1,52 @@
import './styles.scss';
import { ExpandAltOutlined } from '@ant-design/icons';
import LogDetail from 'components/LogDetail';
import dayjs from 'dayjs';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { ILog } from 'types/api/logs/log';
function LogsList({ logs }: LogsListProps): JSX.Element {
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
return (
<div className="logs-preview-list-container">
{logs.map((log) => (
<div key={log.id} className="logs-preview-list-item">
<div className="logs-preview-list-item-timestamp">
{dayjs(String(log.timestamp)).format('MMM DD HH:mm:ss.SSS')}
</div>
<div className="logs-preview-list-item-body">{log.body}</div>
<div
className="logs-preview-list-item-expand"
onClick={makeLogDetailsHandler(log)}
role="button"
tabIndex={0}
onKeyUp={makeLogDetailsHandler(log)}
>
<ExpandAltOutlined />
</div>
</div>
))}
<LogDetail
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</div>
);
}
interface LogsListProps {
logs: ILog[];
}
export default LogsList;

View File

@ -0,0 +1,46 @@
.logs-preview-list-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
box-sizing: border-box;
padding: 0.25rem 0.5rem;
}
.logs-preview-list-item {
width: 100%;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
flex-grow: 1;
}
.logs-preview-list-item:not(:first-child) {
border-top: 1px solid rgba(253, 253, 253, 0.12);
}
.logs-preview-list-item-timestamp {
margin-right: 0.75rem;
white-space: nowrap;
}
.logs-preview-list-item-body {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.logs-preview-list-item-expand{
margin-left: 0.75rem;
color: #1677ff;
padding: 0.25rem 0.375rem;
cursor: pointer;
font-size: 12px;
}

View File

@ -0,0 +1,55 @@
import './styles.scss';
import {
initialFilters,
initialQueriesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import cloneDeep from 'lodash-es/cloneDeep';
import { useMemo } from 'react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
function LogsCountInInterval({
filter,
timeInterval,
}: LogsCountInIntervalProps): JSX.Element | null {
const query = useMemo(() => {
const q = cloneDeep(initialQueriesMap.logs);
q.builder.queryData[0] = {
...q.builder.queryData[0],
filters: filter || initialFilters,
aggregateOperator: LogsAggregatorOperator.COUNT,
};
return q;
}, [filter]);
const result = useGetQueryRange({
graphType: PANEL_TYPES.TABLE,
query,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: timeInterval,
});
if (!result.isFetched) {
return null;
}
const count =
result?.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0]
?.values?.[0]?.value;
return (
<div className="logs-filter-preview-matched-logs-count">
{count} matches in
</div>
);
}
interface LogsCountInIntervalProps {
filter: TagFilter;
timeInterval: Time;
}
export default LogsCountInInterval;

View File

@ -0,0 +1,3 @@
.logs-filter-preview-matched-logs-count {
margin-right: 0.5rem;
}

View File

@ -0,0 +1,45 @@
import './styles.scss';
import { Select } from 'antd';
import {
RelativeDurationOptions,
Time,
} from 'container/TopNav/DateTimeSelection/config';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import LogsCountInInterval from './components/LogsCountInInterval';
function PreviewIntervalSelector({
previewFilter,
value,
onChange,
}: PreviewIntervalSelectorProps): JSX.Element {
const onSelectInterval = (value: unknown): void => onChange(value as Time);
const isEmptyFilter = (previewFilter?.items?.length || 0) < 1;
return (
<div className="logs-filter-preview-time-interval-summary">
{!isEmptyFilter && (
<LogsCountInInterval filter={previewFilter} timeInterval={value} />
)}
<div>
<Select value={value} onSelect={onSelectInterval}>
{RelativeDurationOptions.map(({ value, label }) => (
<Select.Option key={value + label} value={value}>
{label}
</Select.Option>
))}
</Select>
</div>
</div>
);
}
interface PreviewIntervalSelectorProps {
value: Time;
onChange: (interval: Time) => void;
previewFilter: TagFilter;
}
export default PreviewIntervalSelector;

View File

@ -0,0 +1,4 @@
.logs-filter-preview-time-interval-summary {
display: flex;
align-items: center;
}

View File

@ -0,0 +1,76 @@
import './styles.scss';
import {
initialFilters,
initialQueriesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import cloneDeep from 'lodash-es/cloneDeep';
import { useMemo } from 'react';
import { ILog } from 'types/api/logs/log';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
import LogsList from '../LogsList';
function SampleLogs({ filter, timeInterval }: SampleLogsProps): JSX.Element {
const sampleLogsQuery = useMemo(() => {
const q = cloneDeep(initialQueriesMap.logs);
q.builder.queryData[0] = {
...q.builder.queryData[0],
filters: filter || initialFilters,
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
limit: 5,
};
return q;
}, [filter]);
const sampleLogsResponse = useGetQueryRange({
graphType: PANEL_TYPES.LIST,
query: sampleLogsQuery,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: timeInterval,
});
if (sampleLogsResponse?.isError) {
return (
<div className="sample-logs-notice-container">
could not fetch logs for filter
</div>
);
}
if (sampleLogsResponse?.isFetching) {
return <div className="sample-logs-notice-container">Loading...</div>;
}
if ((filter?.items?.length || 0) < 1) {
return (
<div className="sample-logs-notice-container">Please select a filter</div>
);
}
const logsList =
sampleLogsResponse?.data?.payload?.data?.newResult?.data?.result[0]?.list ||
[];
if (logsList.length < 1) {
return <div className="sample-logs-notice-container">No logs found</div>;
}
const logs: ILog[] = logsList.map((item) => ({
...item.data,
timestamp: item.timestamp,
}));
return <LogsList logs={logs} />;
}
interface SampleLogsProps {
filter: TagFilter;
timeInterval: Time;
}
export default SampleLogs;

View File

@ -0,0 +1,7 @@
.sample-logs-notice-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -3,9 +3,9 @@ import './styles.scss';
import { queryFilterTags } from 'hooks/queryBuilder/useTag';
import { PipelineData } from 'types/api/pipeline/def';
function PipelineFilterPreview({
function PipelineFilterSummary({
filter,
}: PipelineFilterPreviewProps): JSX.Element {
}: PipelineFilterSummaryProps): JSX.Element {
return (
<div className="pipeline-filter-preview-container">
{queryFilterTags(filter).map((tag) => (
@ -17,8 +17,8 @@ function PipelineFilterPreview({
);
}
interface PipelineFilterPreviewProps {
interface PipelineFilterSummaryProps {
filter: PipelineData['filter'];
}
export default PipelineFilterPreview;
export default PipelineFilterSummary;

View File

@ -4,7 +4,7 @@ import { PipelineData, ProcessorData } from 'types/api/pipeline/def';
import { PipelineIndexIcon } from '../AddNewProcessor/styles';
import { ColumnDataStyle, ListDataStyle, ProcessorIndexIcon } from '../styles';
import PipelineFilterPreview from './PipelineFilterPreview';
import PipelineFilterSummary from './PipelineFilterSummary';
const componentMap: ComponentMap = {
orderId: ({ record }) => <PipelineIndexIcon>{record}</PipelineIndexIcon>,
@ -15,7 +15,7 @@ const componentMap: ComponentMap = {
),
id: ({ record }) => <ProcessorIndexIcon>{record}</ProcessorIndexIcon>,
name: ({ record }) => <ListDataStyle>{record}</ListDataStyle>,
filter: ({ record }) => <PipelineFilterPreview filter={record} />,
filter: ({ record }) => <PipelineFilterSummary filter={record} />,
};
function TableComponents({

View File

@ -14,25 +14,25 @@ import NameInput from './AddNewPipeline/FormFields/NameInput';
export const pipelineFields = [
{
id: 1,
fieldName: 'Filter',
placeholder: 'pipeline_filter_placeholder',
name: 'filter',
component: FilterInput,
},
{
id: 2,
fieldName: 'Name',
placeholder: 'pipeline_name_placeholder',
name: 'name',
component: NameInput,
},
{
id: 4,
id: 2,
fieldName: 'Description',
placeholder: 'pipeline_description_placeholder',
name: 'description',
component: DescriptionTextArea,
},
{
id: 3,
fieldName: 'Filter',
placeholder: 'pipeline_filter_placeholder',
name: 'filter',
component: FilterInput,
},
];
export const tagInputStyle: React.CSSProperties = {

View File

@ -41,7 +41,7 @@ export interface Option {
label: string;
}
export const ServiceMapOptions: Option[] = [
export const RelativeDurationOptions: Option[] = [
{ value: '5min', label: 'Last 5 min' },
{ value: '15min', label: 'Last 15 min' },
{ value: '30min', label: 'Last 30 min' },
@ -53,7 +53,7 @@ export const ServiceMapOptions: Option[] = [
export const getDefaultOption = (route: string): Time => {
if (route === ROUTES.SERVICE_MAP) {
return ServiceMapOptions[2].value;
return RelativeDurationOptions[2].value;
}
if (route === ROUTES.APPLICATION) {
return Options[2].value;
@ -63,7 +63,7 @@ export const getDefaultOption = (route: string): Time => {
export const getOptions = (routes: string): Option[] => {
if (routes === ROUTES.SERVICE_MAP) {
return ServiceMapOptions;
return RelativeDurationOptions;
}
return Options;
};