mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
1439 lines
43 KiB
TypeScript
1439 lines
43 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import type { Draft } from 'immer';
|
|
import produce from 'immer';
|
|
import { Button as BSButton, Form, InputGroup } from 'react-bootstrap';
|
|
import Select from 'react-select';
|
|
import {
|
|
Button,
|
|
Divider,
|
|
Flex,
|
|
Group,
|
|
Paper,
|
|
Switch,
|
|
Tooltip,
|
|
} from '@mantine/core';
|
|
|
|
import { NumberFormatInput } from './components/NumberFormat';
|
|
import { intervalToGranularity } from './Alert';
|
|
import {
|
|
AGG_FNS,
|
|
ChartSeriesFormCompact,
|
|
convertDateRangeToGranularityString,
|
|
FieldSelect,
|
|
Granularity,
|
|
GroupBySelect,
|
|
seriesToSearchQuery,
|
|
TableSelect,
|
|
} from './ChartUtils';
|
|
import Checkbox from './Checkbox';
|
|
import * as config from './config';
|
|
import EditChartFormAlerts from './EditChartFormAlerts';
|
|
import GranularityPicker from './GranularityPicker';
|
|
import HDXHistogramChart from './HDXHistogramChart';
|
|
import HDXMarkdownChart from './HDXMarkdownChart';
|
|
import HDXMultiSeriesTableChart from './HDXMultiSeriesTableChart';
|
|
import HDXMultiSeriesTimeChart from './HDXMultiSeriesTimeChart';
|
|
import HDXNumberChart from './HDXNumberChart';
|
|
import { LogTableWithSidePanel } from './LogTableWithSidePanel';
|
|
import SearchTimeRangePicker from './SearchTimeRangePicker';
|
|
import type { Alert, Chart, ChartSeries, TimeChartSeries } from './types';
|
|
import { useDebounce } from './utils';
|
|
|
|
const DEFAULT_ALERT: Alert = {
|
|
channel: {
|
|
type: 'webhook',
|
|
},
|
|
threshold: 1,
|
|
interval: '5m',
|
|
type: 'presence',
|
|
source: 'CHART',
|
|
};
|
|
|
|
const buildAndWhereClause = (query1 = '', query2 = '') => {
|
|
if (!query1 && !query2) {
|
|
return '';
|
|
} else if (!query1) {
|
|
return query2;
|
|
} else if (!query2) {
|
|
return query1;
|
|
} else {
|
|
return `${query1} (${query2})`;
|
|
}
|
|
};
|
|
|
|
const withDashboardFilter = <
|
|
T extends { where?: string; series?: ChartSeries[] },
|
|
>(
|
|
chartConfig: T | null,
|
|
dashboardQuery?: string,
|
|
) => {
|
|
return chartConfig
|
|
? {
|
|
...chartConfig,
|
|
where: buildAndWhereClause(dashboardQuery, chartConfig?.where),
|
|
series: chartConfig?.series
|
|
? chartConfig.series.map(s => ({
|
|
...s,
|
|
where: buildAndWhereClause(
|
|
dashboardQuery,
|
|
s.type === 'number' || s.type === 'time' || s.type === 'table'
|
|
? s.where
|
|
: '',
|
|
),
|
|
}))
|
|
: undefined,
|
|
}
|
|
: null;
|
|
};
|
|
|
|
const DashboardFilterApplied = ({
|
|
dashboardQuery,
|
|
}: {
|
|
dashboardQuery: string;
|
|
}) => (
|
|
<Tooltip label="Dashboard filter is applied" color="gray">
|
|
<span className="d-inline-block">
|
|
<i className="bi bi-funnel-fill ms-2 me-1 text-success fs-8.5" />
|
|
<span
|
|
className="d-inline-block lh-1 text-slate-400 fs-8.5 me-2 text-truncate"
|
|
style={{ maxWidth: 340 }}
|
|
>
|
|
{dashboardQuery}
|
|
</span>
|
|
</span>
|
|
</Tooltip>
|
|
);
|
|
|
|
export const EditMarkdownChartForm = ({
|
|
chart,
|
|
onClose,
|
|
onSave,
|
|
}: {
|
|
chart: Chart | undefined;
|
|
onSave?: (chart: Chart) => void;
|
|
onClose?: () => void;
|
|
}) => {
|
|
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
|
|
|
|
const chartConfig = useMemo(() => {
|
|
return editedChart != null && editedChart.series[0].type === 'markdown'
|
|
? {
|
|
content: editedChart.series[0].content,
|
|
}
|
|
: null;
|
|
}, [editedChart]);
|
|
const previewConfig = chartConfig;
|
|
|
|
if (
|
|
chartConfig == null ||
|
|
editedChart == null ||
|
|
previewConfig == null ||
|
|
editedChart.series[0].type !== 'markdown'
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const labelWidth = 320;
|
|
|
|
return (
|
|
<form
|
|
onSubmit={e => {
|
|
e.preventDefault();
|
|
onSave?.(editedChart);
|
|
}}
|
|
>
|
|
<div className="fs-5 mb-4">Markdown</div>
|
|
<div className="d-flex align-items-center mb-4">
|
|
<Form.Control
|
|
type="text"
|
|
id="name"
|
|
onChange={e =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
draft.name = e.target.value;
|
|
}),
|
|
)
|
|
}
|
|
defaultValue={editedChart.name}
|
|
placeholder="Title"
|
|
/>
|
|
</div>
|
|
<div className="d-flex mt-3 align-items-center">
|
|
<div style={{ width: labelWidth }} className="text-muted fw-500 ps-2">
|
|
Content
|
|
</div>
|
|
<div className="ms-3 flex-grow-1">
|
|
<InputGroup>
|
|
<Form.Control
|
|
as="textarea"
|
|
type="text"
|
|
placeholder={'Markdown content'}
|
|
className="border-0 fs-7"
|
|
value={editedChart.series[0].content}
|
|
onChange={event =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
if (draft.series[0].type === 'markdown') {
|
|
draft.series[0].content = event.target.value;
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
/>
|
|
</InputGroup>
|
|
</div>
|
|
</div>
|
|
{(onSave != null || onClose != null) && (
|
|
<div className="d-flex justify-content-between my-3 ps-2">
|
|
{onSave != null && (
|
|
<BSButton
|
|
variant="outline-success"
|
|
className="fs-7 text-muted-hover-black"
|
|
type="submit"
|
|
>
|
|
Save
|
|
</BSButton>
|
|
)}
|
|
{onClose != null && (
|
|
<BSButton onClick={onClose} variant="dark">
|
|
Cancel
|
|
</BSButton>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="mt-4">
|
|
<div className="mb-3 text-muted ps-2 fs-7">Markdown Preview</div>
|
|
<div style={{ height: 400 }} className="bg-hdx-dark">
|
|
<HDXMarkdownChart config={previewConfig} />
|
|
</div>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export const EditSearchChartForm = ({
|
|
chart,
|
|
onClose,
|
|
onSave,
|
|
dateRange,
|
|
dashboardQuery,
|
|
}: {
|
|
chart: Chart | undefined;
|
|
dateRange: [Date, Date];
|
|
onSave?: (chart: Chart) => void;
|
|
onClose?: () => void;
|
|
dashboardQuery?: string;
|
|
}) => {
|
|
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
|
|
|
|
const chartConfig = useMemo(() => {
|
|
return editedChart != null && editedChart.series[0].type === 'search'
|
|
? {
|
|
where: editedChart.series[0].where,
|
|
dateRange,
|
|
}
|
|
: null;
|
|
}, [editedChart, dateRange]);
|
|
const previewConfig = useDebounce(
|
|
withDashboardFilter(chartConfig, dashboardQuery),
|
|
500,
|
|
);
|
|
|
|
if (
|
|
chartConfig == null ||
|
|
editedChart == null ||
|
|
previewConfig == null ||
|
|
editedChart.series[0].type !== 'search'
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const labelWidth = 320;
|
|
|
|
return (
|
|
<form
|
|
onSubmit={e => {
|
|
e.preventDefault();
|
|
onSave?.(editedChart);
|
|
}}
|
|
>
|
|
<div className="fs-5 mb-4">Search Builder</div>
|
|
<div className="d-flex align-items-center mb-4">
|
|
<Form.Control
|
|
type="text"
|
|
id="name"
|
|
onChange={e =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
draft.name = e.target.value;
|
|
}),
|
|
)
|
|
}
|
|
defaultValue={editedChart.name}
|
|
placeholder="Chart Name"
|
|
/>
|
|
</div>
|
|
<div className="d-flex mt-3 align-items-center">
|
|
<div style={{ width: labelWidth }} className="text-muted fw-500 ps-2">
|
|
Search Query
|
|
</div>
|
|
<div className="ms-3 flex-grow-1">
|
|
<InputGroup>
|
|
<Form.Control
|
|
type="text"
|
|
placeholder={'Filter results by a search query'}
|
|
className="border-0 fs-7"
|
|
value={editedChart.series[0].where}
|
|
onChange={event =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
if (draft.series[0].type === 'search') {
|
|
draft.series[0].where = event.target.value;
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
/>
|
|
</InputGroup>
|
|
</div>
|
|
</div>
|
|
{(onSave != null || onClose != null) && (
|
|
<div className="d-flex justify-content-between my-3 ps-2">
|
|
{onSave != null && (
|
|
<BSButton
|
|
variant="outline-success"
|
|
className="fs-7 text-muted-hover-black"
|
|
type="submit"
|
|
>
|
|
Save
|
|
</BSButton>
|
|
)}
|
|
{onClose != null && (
|
|
<BSButton onClick={onClose} variant="dark">
|
|
Cancel
|
|
</BSButton>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="mt-4">
|
|
<div className="mb-3 text-muted ps-2 fs-7">
|
|
Search Preview
|
|
{!!dashboardQuery && (
|
|
<DashboardFilterApplied dashboardQuery={dashboardQuery} />
|
|
)}
|
|
</div>
|
|
<div style={{ height: 400 }} className="bg-hdx-dark">
|
|
<LogTableWithSidePanel
|
|
config={{
|
|
...previewConfig,
|
|
where: previewConfig.where,
|
|
}}
|
|
isLive={false}
|
|
isUTC={false}
|
|
setIsUTC={() => {}}
|
|
onPropertySearchClick={() => {}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export const EditNumberChartForm = ({
|
|
chart,
|
|
onClose,
|
|
onSave,
|
|
dateRange,
|
|
editedChart,
|
|
setEditedChart,
|
|
displayedTimeInputValue,
|
|
setDisplayedTimeInputValue,
|
|
onTimeRangeSearch,
|
|
dashboardQuery,
|
|
}: {
|
|
chart: Chart | undefined;
|
|
dateRange: [Date, Date];
|
|
onSave?: (chart: Chart) => void;
|
|
onClose?: () => void;
|
|
displayedTimeInputValue?: string;
|
|
setDisplayedTimeInputValue?: (value: string) => void;
|
|
onTimeRangeSearch?: (value: string) => void;
|
|
editedChart?: Chart;
|
|
setEditedChart?: (chart: Chart) => void;
|
|
dashboardQuery?: string;
|
|
}) => {
|
|
const [editedChartState, setEditedChartState] = useState<Chart | undefined>(
|
|
chart,
|
|
);
|
|
const [_editedChart, _setEditedChart] =
|
|
editedChart != null && setEditedChart != null
|
|
? [editedChart, setEditedChart]
|
|
: [editedChartState, setEditedChartState];
|
|
|
|
const chartConfig = useMemo(() => {
|
|
return _editedChart != null && _editedChart.series[0].type === 'number'
|
|
? {
|
|
field: _editedChart.series[0].field ?? '', // TODO: Fix in definition
|
|
dateRange,
|
|
numberFormat: _editedChart.series[0].numberFormat,
|
|
series: _editedChart.series,
|
|
granularity: convertDateRangeToGranularityString(dateRange, 60),
|
|
}
|
|
: null;
|
|
}, [_editedChart, dateRange]);
|
|
const previewConfig = useDebounce(
|
|
withDashboardFilter(chartConfig, dashboardQuery),
|
|
500,
|
|
);
|
|
|
|
if (
|
|
chartConfig == null ||
|
|
_editedChart == null ||
|
|
previewConfig == null ||
|
|
_editedChart.series[0].type !== 'number'
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<form
|
|
onSubmit={e => {
|
|
e.preventDefault();
|
|
onSave?.(_editedChart);
|
|
}}
|
|
>
|
|
<div className="fs-5 mb-4">Number Tile Builder</div>
|
|
<div className="d-flex align-items-center mb-4">
|
|
<Form.Control
|
|
type="text"
|
|
id="name"
|
|
onChange={e =>
|
|
_setEditedChart(
|
|
produce(_editedChart, draft => {
|
|
draft.name = e.target.value;
|
|
}),
|
|
)
|
|
}
|
|
defaultValue={_editedChart.name}
|
|
placeholder="Chart Name"
|
|
/>
|
|
</div>
|
|
<EditMultiSeriesChartForm
|
|
{...{
|
|
editedChart: _editedChart,
|
|
setEditedChart: _setEditedChart,
|
|
chartType: 'number',
|
|
}}
|
|
/>
|
|
|
|
{(onSave != null || onClose != null) && (
|
|
<div className="d-flex justify-content-between my-3 ps-2">
|
|
{onSave != null && (
|
|
<BSButton
|
|
variant="outline-success"
|
|
className="fs-7 text-muted-hover-black"
|
|
type="submit"
|
|
>
|
|
Save
|
|
</BSButton>
|
|
)}
|
|
{onClose != null && (
|
|
<BSButton onClick={onClose} variant="dark">
|
|
Cancel
|
|
</BSButton>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="mt-4">
|
|
<Flex justify="space-between" align="center" mb="sm">
|
|
<div className="text-muted ps-2 fs-7" style={{ flexGrow: 1 }}>
|
|
Chart Preview
|
|
{!!dashboardQuery && (
|
|
<DashboardFilterApplied dashboardQuery={dashboardQuery} />
|
|
)}
|
|
</div>
|
|
{setDisplayedTimeInputValue != null &&
|
|
displayedTimeInputValue != null &&
|
|
onTimeRangeSearch != null && (
|
|
<div className="ms-3 flex-grow-1" style={{ maxWidth: 360 }}>
|
|
<SearchTimeRangePicker
|
|
inputValue={displayedTimeInputValue}
|
|
setInputValue={setDisplayedTimeInputValue}
|
|
onSearch={range => {
|
|
onTimeRangeSearch(range);
|
|
}}
|
|
onSubmit={range => {
|
|
onTimeRangeSearch(range);
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Flex>
|
|
<div style={{ height: 400 }}>
|
|
<HDXNumberChart config={previewConfig} />
|
|
</div>
|
|
</div>
|
|
{_editedChart.series[0].table === 'logs' ? (
|
|
<>
|
|
<div className="ps-2 mt-2 border-top border-dark">
|
|
<div className="my-3 fs-7 fw-bold">Sample Matched Events</div>
|
|
<div style={{ height: 150 }} className="bg-hdx-dark">
|
|
<LogTableWithSidePanel
|
|
config={{
|
|
...previewConfig,
|
|
where: `${previewConfig.where} ${
|
|
previewConfig.field != '' ? `${previewConfig.field}:*` : ''
|
|
}`,
|
|
}}
|
|
isLive={false}
|
|
isUTC={false}
|
|
setIsUTC={() => {}}
|
|
onPropertySearchClick={() => {}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export const EditTableChartForm = ({
|
|
chart,
|
|
onClose,
|
|
onSave,
|
|
dateRange,
|
|
displayedTimeInputValue,
|
|
setDisplayedTimeInputValue,
|
|
onTimeRangeSearch,
|
|
editedChart,
|
|
setEditedChart,
|
|
dashboardQuery,
|
|
}: {
|
|
chart: Chart | undefined;
|
|
dateRange: [Date, Date];
|
|
onSave?: (chart: Chart) => void;
|
|
onClose?: () => void;
|
|
editedChart?: Chart;
|
|
setEditedChart?: (chart: Chart) => void;
|
|
displayedTimeInputValue?: string;
|
|
setDisplayedTimeInputValue?: (value: string) => void;
|
|
onTimeRangeSearch?: (value: string) => void;
|
|
dashboardQuery?: string;
|
|
}) => {
|
|
const CHART_TYPE = 'table';
|
|
|
|
const [editedChartState, setEditedChartState] = useState<Chart | undefined>(
|
|
chart,
|
|
);
|
|
const [_editedChart, _setEditedChart] =
|
|
editedChart != null && setEditedChart != null
|
|
? [editedChart, setEditedChart]
|
|
: [editedChartState, setEditedChartState];
|
|
|
|
const chartConfig = useMemo(
|
|
() =>
|
|
_editedChart != null && _editedChart.series?.[0]?.type === CHART_TYPE
|
|
? {
|
|
table: _editedChart.series[0].table ?? 'logs',
|
|
aggFn: _editedChart.series[0].aggFn,
|
|
field: _editedChart.series[0].field ?? '', // TODO: Fix in definition
|
|
groupBy: _editedChart.series[0].groupBy[0],
|
|
where: _editedChart.series[0].where,
|
|
sortOrder: _editedChart.series[0].sortOrder ?? 'desc',
|
|
granularity: convertDateRangeToGranularityString(dateRange, 60),
|
|
dateRange,
|
|
numberFormat: _editedChart.series[0].numberFormat,
|
|
series: _editedChart.series,
|
|
seriesReturnType: _editedChart.seriesReturnType,
|
|
}
|
|
: null,
|
|
[_editedChart, dateRange],
|
|
);
|
|
const previewConfig = useDebounce(
|
|
withDashboardFilter(chartConfig, dashboardQuery),
|
|
500,
|
|
);
|
|
|
|
if (
|
|
chartConfig == null ||
|
|
previewConfig == null ||
|
|
_editedChart == null ||
|
|
_editedChart.series[0].type !== CHART_TYPE
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<form
|
|
className="flex-grow-1 d-flex flex-column"
|
|
onSubmit={e => {
|
|
e.preventDefault();
|
|
onSave?.(_editedChart);
|
|
}}
|
|
>
|
|
<div className="fs-5 mb-4">Table Builder</div>
|
|
<div className="d-flex align-items-center mb-4">
|
|
<Form.Control
|
|
type="text"
|
|
id="name"
|
|
onChange={e =>
|
|
_setEditedChart(
|
|
produce(_editedChart, draft => {
|
|
draft.name = e.target.value;
|
|
}),
|
|
)
|
|
}
|
|
defaultValue={_editedChart.name}
|
|
placeholder="Chart Name"
|
|
/>
|
|
</div>
|
|
<EditMultiSeriesChartForm
|
|
{...{
|
|
editedChart: _editedChart,
|
|
setEditedChart: _setEditedChart,
|
|
chartType: CHART_TYPE,
|
|
}}
|
|
/>
|
|
{(onSave != null || onClose != null) && (
|
|
<div className="d-flex justify-content-between my-3 ps-2">
|
|
{onSave != null && (
|
|
<BSButton
|
|
variant="outline-success"
|
|
className="fs-7 text-muted-hover-black"
|
|
type="submit"
|
|
>
|
|
Save
|
|
</BSButton>
|
|
)}
|
|
{onClose != null && (
|
|
<BSButton onClick={onClose} variant="dark">
|
|
Cancel
|
|
</BSButton>
|
|
)}
|
|
</div>
|
|
)}
|
|
<Flex justify="space-between" align="center" mb="sm">
|
|
<div className="text-muted ps-2 fs-7" style={{ flexGrow: 1 }}>
|
|
Chart Preview
|
|
{!!dashboardQuery && (
|
|
<DashboardFilterApplied dashboardQuery={dashboardQuery} />
|
|
)}
|
|
</div>
|
|
{setDisplayedTimeInputValue != null &&
|
|
displayedTimeInputValue != null &&
|
|
onTimeRangeSearch != null && (
|
|
<div className="ms-3 flex-grow-1" style={{ maxWidth: 360 }}>
|
|
<SearchTimeRangePicker
|
|
inputValue={displayedTimeInputValue}
|
|
setInputValue={setDisplayedTimeInputValue}
|
|
onSearch={range => {
|
|
onTimeRangeSearch(range);
|
|
}}
|
|
onSubmit={range => {
|
|
onTimeRangeSearch(range);
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Flex>
|
|
<div
|
|
style={{ minHeight: 400 }}
|
|
className="d-flex flex-column flex-grow-1"
|
|
>
|
|
<HDXMultiSeriesTableChart
|
|
config={previewConfig}
|
|
onSortClick={seriesIndex => {
|
|
_setEditedChart(
|
|
produce(_editedChart, draft => {
|
|
// We need to clear out all other series sort orders first
|
|
for (let i = 0; i < draft.series.length; i++) {
|
|
if (i !== seriesIndex) {
|
|
const s = draft.series[i];
|
|
if (s.type === CHART_TYPE) {
|
|
s.sortOrder = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
const s = draft.series[seriesIndex];
|
|
if (s.type === CHART_TYPE) {
|
|
s.sortOrder =
|
|
s.sortOrder == null
|
|
? 'desc'
|
|
: s.sortOrder === 'asc'
|
|
? 'desc'
|
|
: 'asc';
|
|
}
|
|
|
|
return;
|
|
}),
|
|
);
|
|
}}
|
|
/>
|
|
</div>
|
|
{_editedChart.series[0].table === 'logs' ? (
|
|
<>
|
|
<div className="ps-2 mt-2 border-top border-dark">
|
|
<div className="my-3 fs-7 fw-bold">Sample Matched Events</div>
|
|
<div style={{ height: 150 }} className="bg-hdx-dark">
|
|
<LogTableWithSidePanel
|
|
config={{
|
|
...previewConfig,
|
|
where: `${previewConfig.where} ${
|
|
previewConfig.aggFn != 'count' && previewConfig.field != ''
|
|
? `${previewConfig.field}:*`
|
|
: ''
|
|
} ${
|
|
previewConfig.groupBy != '' && previewConfig.groupBy != null
|
|
? `${previewConfig.groupBy}:*`
|
|
: ''
|
|
}`,
|
|
}}
|
|
isLive={false}
|
|
isUTC={false}
|
|
setIsUTC={() => {}}
|
|
onPropertySearchClick={() => {}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export const EditHistogramChartForm = ({
|
|
chart,
|
|
onClose,
|
|
onSave,
|
|
dateRange,
|
|
displayedTimeInputValue,
|
|
setDisplayedTimeInputValue,
|
|
onTimeRangeSearch,
|
|
editedChart,
|
|
setEditedChart,
|
|
dashboardQuery,
|
|
}: {
|
|
chart: Chart | undefined;
|
|
dateRange: [Date, Date];
|
|
onSave?: (chart: Chart) => void;
|
|
onClose?: () => void;
|
|
editedChart?: Chart;
|
|
setEditedChart?: (chart: Chart) => void;
|
|
displayedTimeInputValue?: string;
|
|
setDisplayedTimeInputValue?: (value: string) => void;
|
|
onTimeRangeSearch?: (value: string) => void;
|
|
dashboardQuery?: string;
|
|
}) => {
|
|
const [editedChartState, setEditedChartState] = useState<Chart | undefined>(
|
|
chart,
|
|
);
|
|
const [_editedChart, _setEditedChart] =
|
|
editedChart != null && setEditedChart != null
|
|
? [editedChart, setEditedChart]
|
|
: [editedChartState, setEditedChartState];
|
|
|
|
const chartConfig = useMemo(() => {
|
|
return _editedChart != null && _editedChart.series[0].type === 'histogram'
|
|
? {
|
|
table: _editedChart.series[0].table ?? 'logs',
|
|
field: _editedChart.series[0].field ?? '', // TODO: Fix in definition
|
|
where: _editedChart.series[0].where,
|
|
dateRange,
|
|
}
|
|
: null;
|
|
}, [_editedChart, dateRange]);
|
|
const previewConfig = useDebounce(
|
|
withDashboardFilter(chartConfig, dashboardQuery),
|
|
500,
|
|
);
|
|
|
|
if (
|
|
chartConfig == null ||
|
|
_editedChart == null ||
|
|
previewConfig == null ||
|
|
_editedChart.series[0].type !== 'histogram'
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const labelWidth = 320;
|
|
|
|
return (
|
|
<form
|
|
onSubmit={e => {
|
|
e.preventDefault();
|
|
onSave?.(_editedChart);
|
|
}}
|
|
>
|
|
<div className="fs-5 mb-4">Histogram Builder</div>
|
|
<div className="d-flex align-items-center mb-4">
|
|
<Form.Control
|
|
type="text"
|
|
id="name"
|
|
onChange={e =>
|
|
_setEditedChart(
|
|
produce(_editedChart, draft => {
|
|
draft.name = e.target.value;
|
|
}),
|
|
)
|
|
}
|
|
defaultValue={_editedChart.name}
|
|
placeholder="Chart Name"
|
|
/>
|
|
</div>
|
|
<div className="d-flex mt-3 align-items-center">
|
|
<div style={{ width: labelWidth }} className="text-muted fw-500 ps-2">
|
|
Field
|
|
</div>
|
|
<div className="ms-3 flex-grow-1">
|
|
<FieldSelect
|
|
value={_editedChart.series[0].field ?? ''}
|
|
setValue={field =>
|
|
_setEditedChart(
|
|
produce(_editedChart, draft => {
|
|
if (draft.series[0].type === 'histogram') {
|
|
draft.series[0].field = field;
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
types={['number']}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="d-flex mt-3 align-items-center">
|
|
<div style={{ width: labelWidth }} className="text-muted fw-500 ps-2">
|
|
Where
|
|
</div>
|
|
<div className="ms-3 flex-grow-1">
|
|
<InputGroup>
|
|
<Form.Control
|
|
type="text"
|
|
placeholder={'Filter results by a search query'}
|
|
className="border-0 fs-7"
|
|
value={_editedChart.series[0].where}
|
|
onChange={event =>
|
|
_setEditedChart(
|
|
produce(_editedChart, draft => {
|
|
if (draft.series[0].type === 'histogram') {
|
|
draft.series[0].where = event.target.value;
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
/>
|
|
</InputGroup>
|
|
</div>
|
|
</div>
|
|
{(onSave != null || onClose != null) && (
|
|
<div className="d-flex justify-content-between my-3 ps-2">
|
|
{onSave != null && (
|
|
<BSButton
|
|
variant="outline-success"
|
|
className="fs-7 text-muted-hover-black"
|
|
type="submit"
|
|
>
|
|
Save
|
|
</BSButton>
|
|
)}
|
|
{onClose != null && (
|
|
<BSButton onClick={onClose} variant="dark">
|
|
Cancel
|
|
</BSButton>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="mt-4">
|
|
<Flex justify="space-between" align="center" mb="sm">
|
|
<div className="text-muted ps-2 fs-7" style={{ flexGrow: 1 }}>
|
|
Chart Preview
|
|
{!!dashboardQuery && (
|
|
<DashboardFilterApplied dashboardQuery={dashboardQuery} />
|
|
)}
|
|
</div>
|
|
{setDisplayedTimeInputValue != null &&
|
|
displayedTimeInputValue != null &&
|
|
onTimeRangeSearch != null && (
|
|
<div className="ms-3 flex-grow-1" style={{ maxWidth: 360 }}>
|
|
<SearchTimeRangePicker
|
|
inputValue={displayedTimeInputValue}
|
|
setInputValue={setDisplayedTimeInputValue}
|
|
onSearch={range => {
|
|
onTimeRangeSearch(range);
|
|
}}
|
|
onSubmit={range => {
|
|
onTimeRangeSearch(range);
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Flex>
|
|
<div style={{ height: 400 }}>
|
|
<HDXHistogramChart config={previewConfig} />
|
|
</div>
|
|
</div>
|
|
{_editedChart.series[0].table === 'logs' ? (
|
|
<>
|
|
<div className="ps-2 mt-2 border-top border-dark">
|
|
<div className="my-3 fs-7 fw-bold">Sample Matched Events</div>
|
|
<div style={{ height: 150 }} className="bg-hdx-dark">
|
|
<LogTableWithSidePanel
|
|
config={{
|
|
...previewConfig,
|
|
where: `${previewConfig.where} ${
|
|
previewConfig.field != '' ? `${previewConfig.field}:*` : ''
|
|
}`,
|
|
}}
|
|
isLive={false}
|
|
isUTC={false}
|
|
setIsUTC={() => {}}
|
|
onPropertySearchClick={() => {}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</form>
|
|
);
|
|
};
|
|
|
|
function pushNewSeries(draft: Draft<Chart>) {
|
|
const firstSeries = draft.series[0] as TimeChartSeries;
|
|
const { table, type, groupBy, numberFormat } = firstSeries;
|
|
draft.series.push({
|
|
table,
|
|
type,
|
|
aggFn: table === 'logs' ? 'count' : 'avg',
|
|
field: '',
|
|
where: '',
|
|
groupBy,
|
|
numberFormat,
|
|
});
|
|
}
|
|
|
|
export const EditMultiSeriesChartForm = ({
|
|
editedChart,
|
|
setEditedChart,
|
|
chartType,
|
|
}: {
|
|
editedChart: Chart;
|
|
setEditedChart: (chart: Chart) => void;
|
|
chartType: 'time' | 'table' | 'number';
|
|
}) => {
|
|
if (editedChart.series[0].type !== chartType) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{editedChart.series.length > 1 && (
|
|
<Flex align="center" gap="md" mb="sm">
|
|
<div className="text-muted">
|
|
<i className="bi bi-database me-2" />
|
|
Data Source
|
|
</div>
|
|
<div className="flex-grow-1">
|
|
<TableSelect
|
|
table={editedChart.series[0].table ?? 'logs'}
|
|
setTableAndAggFn={(table, aggFn) => {
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
draft.series.forEach((series, i) => {
|
|
if (series.type === chartType) {
|
|
series.table = table;
|
|
series.aggFn = aggFn;
|
|
}
|
|
});
|
|
}),
|
|
);
|
|
}}
|
|
/>
|
|
</div>
|
|
</Flex>
|
|
)}
|
|
{editedChart.series.map((series, i) => {
|
|
if (series.type !== chartType) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="mb-2" key={i}>
|
|
<Divider
|
|
label={
|
|
<>
|
|
{editedChart.series.length > 1 && (
|
|
<Button
|
|
variant="subtle"
|
|
color="gray"
|
|
size="xs"
|
|
onClick={() => {
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
draft.series.splice(i, 1);
|
|
if (draft.series.length != 2) {
|
|
draft.seriesReturnType = 'column';
|
|
}
|
|
}),
|
|
);
|
|
}}
|
|
>
|
|
<i className="bi bi-trash me-2" />
|
|
Remove {series.type === 'number' ? 'Ratio' : 'Series'}
|
|
</Button>
|
|
)}
|
|
</>
|
|
}
|
|
c="dark.2"
|
|
labelPosition="right"
|
|
mb={8}
|
|
/>
|
|
<ChartSeriesFormCompact
|
|
table={series.table ?? 'logs'}
|
|
aggFn={series.aggFn}
|
|
where={series.where}
|
|
groupBy={series.type !== 'number' ? series.groupBy[0] : undefined}
|
|
field={series.field ?? ''}
|
|
numberFormat={series.numberFormat}
|
|
setAggFn={aggFn =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
const draftSeries = draft.series[i];
|
|
if (draftSeries.type === chartType) {
|
|
draftSeries.aggFn = aggFn;
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
setWhere={where =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
const draftSeries = draft.series[i];
|
|
if (draftSeries.type === chartType) {
|
|
draftSeries.where = where;
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
setGroupBy={
|
|
editedChart.series.length === 1 && series.type !== 'number'
|
|
? groupBy =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
const draftSeries = draft.series[i];
|
|
if (
|
|
draftSeries.type === chartType &&
|
|
draftSeries.type !== 'number'
|
|
) {
|
|
if (groupBy != undefined) {
|
|
draftSeries.groupBy[0] = groupBy;
|
|
} else {
|
|
draftSeries.groupBy = [];
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
: undefined
|
|
}
|
|
setField={field =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
const draftSeries = draft.series[i];
|
|
if (draftSeries.type === chartType) {
|
|
draftSeries.field = field;
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
setTableAndAggFn={
|
|
editedChart.series.length === 1
|
|
? (table, aggFn) => {
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
const draftSeries = draft.series[i];
|
|
if (draftSeries.type === chartType) {
|
|
draftSeries.table = table;
|
|
draftSeries.aggFn = aggFn;
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
: undefined
|
|
}
|
|
setFieldAndAggFn={(field, aggFn) => {
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
const draftSeries = draft.series[i];
|
|
if (draftSeries.type === chartType) {
|
|
draftSeries.field = field;
|
|
draftSeries.aggFn = aggFn;
|
|
}
|
|
}),
|
|
);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
<Divider my="md" />
|
|
{editedChart.series.length > 1 &&
|
|
editedChart.series[0].type !== 'number' && (
|
|
<Flex align="center" gap="md" mb="sm">
|
|
<div className="text-muted">Group By</div>
|
|
<div className="flex-grow-1">
|
|
<GroupBySelect
|
|
table={editedChart.series[0].table ?? 'logs'}
|
|
groupBy={editedChart.series[0].groupBy[0]}
|
|
fields={
|
|
editedChart.series
|
|
.map(s => (s as TimeChartSeries).field)
|
|
.filter(f => f != null) as string[]
|
|
}
|
|
setGroupBy={groupBy => {
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
draft.series.forEach((series, i) => {
|
|
if (
|
|
series.type === chartType &&
|
|
series.type !== 'number'
|
|
) {
|
|
if (groupBy != undefined) {
|
|
series.groupBy[0] = groupBy;
|
|
} else {
|
|
series.groupBy = [];
|
|
}
|
|
}
|
|
});
|
|
}),
|
|
);
|
|
}}
|
|
/>
|
|
</div>
|
|
</Flex>
|
|
)}
|
|
<Flex justify="space-between">
|
|
<Flex gap="md" align="center">
|
|
{editedChart.series.length === 1 && (
|
|
<Button
|
|
mt={4}
|
|
variant="subtle"
|
|
size="sm"
|
|
color="gray"
|
|
onClick={() => {
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
pushNewSeries(draft);
|
|
draft.seriesReturnType = 'ratio';
|
|
}),
|
|
);
|
|
}}
|
|
>
|
|
<i className="bi bi-plus-circle me-2" />
|
|
Add Ratio
|
|
</Button>
|
|
)}
|
|
{chartType !== 'number' && (
|
|
<Button
|
|
mt={4}
|
|
variant="subtle"
|
|
size="sm"
|
|
color="gray"
|
|
onClick={() => {
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
pushNewSeries(draft);
|
|
draft.seriesReturnType = 'column';
|
|
}),
|
|
);
|
|
}}
|
|
>
|
|
<i className="bi bi-plus-circle me-2" />
|
|
Add Series
|
|
</Button>
|
|
)}
|
|
{editedChart.series.length == 2 && chartType !== 'number' && (
|
|
<Switch
|
|
label="As Ratio"
|
|
checked={editedChart.seriesReturnType === 'ratio'}
|
|
onChange={event =>
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
draft.seriesReturnType = event.currentTarget.checked
|
|
? 'ratio'
|
|
: 'column';
|
|
}),
|
|
)
|
|
}
|
|
/>
|
|
)}
|
|
</Flex>
|
|
<NumberFormatInput
|
|
value={editedChart.series[0].numberFormat}
|
|
onChange={numberFormat => {
|
|
setEditedChart(
|
|
produce(editedChart, draft => {
|
|
draft.series.forEach((series, i) => {
|
|
if (series.type === chartType) {
|
|
series.numberFormat = numberFormat;
|
|
}
|
|
});
|
|
}),
|
|
);
|
|
}}
|
|
/>
|
|
</Flex>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const EditLineChartForm = ({
|
|
isLocalDashboard,
|
|
isAddingAlert,
|
|
chart,
|
|
alerts,
|
|
onClose,
|
|
onSave,
|
|
dateRange,
|
|
editedChart,
|
|
setEditedChart,
|
|
granularity,
|
|
setGranularity,
|
|
setDisplayedTimeInputValue,
|
|
displayedTimeInputValue,
|
|
onTimeRangeSearch,
|
|
dashboardQuery,
|
|
}: {
|
|
isLocalDashboard: boolean;
|
|
isAddingAlert?: boolean;
|
|
chart: Chart | undefined;
|
|
alerts?: Alert[];
|
|
dateRange: [Date, Date];
|
|
onSave?: (chart: Chart, alerts?: Alert[]) => void;
|
|
onClose?: () => void;
|
|
editedChart?: Chart;
|
|
setEditedChart?: (chart: Chart) => void;
|
|
displayedTimeInputValue?: string;
|
|
setDisplayedTimeInputValue?: (value: string) => void;
|
|
onTimeRangeSearch?: (value: string) => void;
|
|
granularity?: Granularity | undefined;
|
|
setGranularity?: (granularity: Granularity | undefined) => void;
|
|
dashboardQuery?: string;
|
|
}) => {
|
|
const CHART_TYPE = 'time';
|
|
const [alert] = alerts ?? []; // TODO: Support multiple alerts eventually
|
|
const [editedChartState, setEditedChartState] = useState<Chart | undefined>(
|
|
chart,
|
|
);
|
|
const [editedAlert, setEditedAlert] = useState<Alert | undefined>(alert);
|
|
const [alertEnabled, setAlertEnabled] = useState(
|
|
editedAlert != null || !!isAddingAlert,
|
|
);
|
|
|
|
const [_editedChart, _setEditedChart] =
|
|
editedChart != null && setEditedChart != null
|
|
? [editedChart, setEditedChart]
|
|
: [editedChartState, setEditedChartState];
|
|
|
|
const chartConfig = useMemo(
|
|
() =>
|
|
_editedChart != null && _editedChart.series?.[0]?.type === CHART_TYPE
|
|
? {
|
|
table: _editedChart.series[0].table ?? 'logs',
|
|
aggFn: _editedChart.series[0].aggFn,
|
|
field: _editedChart.series[0].field ?? '', // TODO: Fix in definition
|
|
groupBy: _editedChart.series[0].groupBy[0],
|
|
where: _editedChart.series[0].where,
|
|
granularity:
|
|
alertEnabled && editedAlert?.interval
|
|
? intervalToGranularity(editedAlert?.interval)
|
|
: granularity ??
|
|
convertDateRangeToGranularityString(dateRange, 60),
|
|
dateRange,
|
|
numberFormat: _editedChart.series[0].numberFormat,
|
|
series: _editedChart.series,
|
|
seriesReturnType: _editedChart.seriesReturnType,
|
|
}
|
|
: null,
|
|
[_editedChart, alertEnabled, editedAlert?.interval, dateRange, granularity],
|
|
);
|
|
const previewConfig = useDebounce(
|
|
withDashboardFilter(chartConfig, dashboardQuery),
|
|
500,
|
|
);
|
|
|
|
if (
|
|
chartConfig == null ||
|
|
previewConfig == null ||
|
|
_editedChart == null ||
|
|
_editedChart.series[0].type !== 'time'
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const isChartAlertsFeatureEnabled =
|
|
alerts != null &&
|
|
((_editedChart.series[0].table ?? 'logs') === 'logs' ||
|
|
_editedChart.series[0].table === 'metrics');
|
|
|
|
return (
|
|
<form
|
|
className="d-flex flex-column flex-grow-1"
|
|
onSubmit={e => {
|
|
e.preventDefault();
|
|
onSave?.(
|
|
_editedChart,
|
|
alertEnabled ? [editedAlert ?? DEFAULT_ALERT] : undefined,
|
|
);
|
|
}}
|
|
>
|
|
<div className="fs-5 mb-4">Line Chart Builder</div>
|
|
<div className="d-flex align-items-center mb-4">
|
|
<Form.Control
|
|
type="text"
|
|
id="name"
|
|
onChange={e =>
|
|
_setEditedChart(
|
|
produce(_editedChart, draft => {
|
|
draft.name = e.target.value;
|
|
}),
|
|
)
|
|
}
|
|
defaultValue={_editedChart.name}
|
|
placeholder="Chart Name"
|
|
/>
|
|
</div>
|
|
<EditMultiSeriesChartForm
|
|
{...{
|
|
editedChart: _editedChart,
|
|
setEditedChart: _setEditedChart,
|
|
chartType: CHART_TYPE,
|
|
}}
|
|
/>
|
|
|
|
{isChartAlertsFeatureEnabled && (
|
|
<Paper
|
|
bg="dark.7"
|
|
p="md"
|
|
py="xs"
|
|
mt="md"
|
|
withBorder
|
|
style={isAddingAlert ? { borderWidth: 3 } : undefined}
|
|
>
|
|
{isLocalDashboard ? (
|
|
<span className="text-gray-600 fs-8">
|
|
Alerts are not available in unsaved dashboards.
|
|
</span>
|
|
) : (
|
|
<>
|
|
<Checkbox
|
|
id="check"
|
|
label="Enable alerts"
|
|
checked={alertEnabled}
|
|
onChange={() => setAlertEnabled(!alertEnabled)}
|
|
/>
|
|
{alertEnabled && (
|
|
<div className="mt-2">
|
|
<Divider mb="sm" />
|
|
<EditChartFormAlerts
|
|
alert={editedAlert ?? DEFAULT_ALERT}
|
|
setAlert={setEditedAlert}
|
|
numberFormat={_editedChart.series[0].numberFormat}
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</Paper>
|
|
)}
|
|
|
|
{(onSave != null || onClose != null) && (
|
|
<div className="d-flex justify-content-between my-3">
|
|
{onSave != null && (
|
|
<BSButton
|
|
variant="outline-success"
|
|
className="fs-7 text-muted-hover-black"
|
|
type="submit"
|
|
>
|
|
Save
|
|
</BSButton>
|
|
)}
|
|
{onClose != null && (
|
|
<BSButton onClick={onClose} variant="dark">
|
|
Cancel
|
|
</BSButton>
|
|
)}
|
|
</div>
|
|
)}
|
|
<Flex justify="space-between" align="center" my="sm">
|
|
<div className="text-muted ps-2 fs-7" style={{ flexGrow: 1 }}>
|
|
Chart Preview
|
|
{!!dashboardQuery && (
|
|
<DashboardFilterApplied dashboardQuery={dashboardQuery} />
|
|
)}
|
|
</div>
|
|
<Flex align="center" style={{ marginLeft: 'auto', width: 600 }}>
|
|
{setDisplayedTimeInputValue != null &&
|
|
displayedTimeInputValue != null &&
|
|
onTimeRangeSearch != null && (
|
|
<div className="ms-3 flex-grow-1" style={{ maxWidth: 420 }}>
|
|
<SearchTimeRangePicker
|
|
inputValue={displayedTimeInputValue}
|
|
setInputValue={setDisplayedTimeInputValue}
|
|
onSearch={range => {
|
|
onTimeRangeSearch(range);
|
|
}}
|
|
onSubmit={range => {
|
|
onTimeRangeSearch(range);
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
{setGranularity != null && (
|
|
<div className="ms-3" style={{ maxWidth: 360 }}>
|
|
<GranularityPicker
|
|
value={granularity}
|
|
onChange={setGranularity}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Flex>
|
|
</Flex>
|
|
|
|
<div
|
|
className="flex-grow-1 d-flex flex-column"
|
|
style={{ minHeight: 400 }}
|
|
>
|
|
<HDXMultiSeriesTimeChart
|
|
config={previewConfig}
|
|
{...(alertEnabled && {
|
|
alertThreshold: editedAlert?.threshold,
|
|
alertThresholdType:
|
|
editedAlert?.type === 'presence' ? 'above' : 'below',
|
|
})}
|
|
/>
|
|
</div>
|
|
{_editedChart.series[0].table === 'logs' ? (
|
|
<>
|
|
<div className="ps-2 mt-2 border-top border-dark">
|
|
<div className="my-3 fs-7 fw-bold">Sample Matched Events</div>
|
|
<div style={{ height: 150 }} className="bg-hdx-dark">
|
|
<LogTableWithSidePanel
|
|
config={{
|
|
...previewConfig,
|
|
where: `${seriesToSearchQuery({
|
|
series: previewConfig.series,
|
|
})}`,
|
|
}}
|
|
isLive={false}
|
|
isUTC={false}
|
|
setIsUTC={() => {}}
|
|
onPropertySearchClick={() => {}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</form>
|
|
);
|
|
};
|