mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-15 14:05:54 +08:00
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:
parent
d7d4000240
commit
9e91375632
@ -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>
|
||||
);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
.pipeline-filter-input-preview-container {
|
||||
margin-top: 1rem;
|
||||
}
|
@ -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;
|
@ -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);
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
@ -0,0 +1,3 @@
|
||||
.logs-filter-preview-matched-logs-count {
|
||||
margin-right: 0.5rem;
|
||||
}
|
@ -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;
|
@ -0,0 +1,4 @@
|
||||
.logs-filter-preview-time-interval-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
@ -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;
|
@ -0,0 +1,7 @@
|
||||
.sample-logs-notice-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
@ -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;
|
@ -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({
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user