mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Unify chart page to use edit chart form in dashboard (#288)
<img width="1552" alt="image" src="https://github.com/hyperdxio/hyperdx/assets/2781687/3330e765-a1c3-4158-b88f-0e20a438e678">
This commit is contained in:
parent
3fc4d52bf9
commit
95ccfa1a51
8 changed files with 759 additions and 646 deletions
6
.changeset/lazy-terms-taste.md
Normal file
6
.changeset/lazy-terms-taste.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@hyperdx/app': patch
|
||||
---
|
||||
|
||||
Add multi-series line/table charts as well as histogram/number charts to the
|
||||
chart explorer.
|
||||
|
|
@ -1,23 +1,13 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import produce from 'immer';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { QueryParamConfig } from 'serialize-query-params';
|
||||
import { decodeArray, encodeArray } from 'serialize-query-params';
|
||||
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
|
||||
|
||||
import { ChartSeriesForm } from './ChartUtils';
|
||||
import DSSelect from './DSSelect';
|
||||
import HDXLineChart from './HDXLineChart';
|
||||
import { Granularity, isGranularity } from './ChartUtils';
|
||||
import EditTileForm from './EditTileForm';
|
||||
import { withAppNav } from './layout';
|
||||
import { LogTableWithSidePanel } from './LogTableWithSidePanel';
|
||||
import SearchTimeRangePicker, {
|
||||
parseTimeRangeInput,
|
||||
} from './SearchTimeRangePicker';
|
||||
import { parseTimeQuery, useTimeQuery } from './timeQuery';
|
||||
import type { AggFn, ChartSeries, SourceTable } from './types';
|
||||
import { parseTimeQuery, useNewTimeQuery } from './timeQuery';
|
||||
import type { Chart, ChartSeries } from './types';
|
||||
import { useQueryParam as useHDXQueryParam } from './useQueryParam';
|
||||
|
||||
export const ChartSeriesParam: QueryParamConfig<ChartSeries[] | undefined> = {
|
||||
|
|
@ -37,10 +27,8 @@ export const ChartSeriesParam: QueryParamConfig<ChartSeries[] | undefined> = {
|
|||
};
|
||||
|
||||
// TODO: This is a hack to set the default time range
|
||||
const defaultTimeRange = parseTimeQuery('Past 1h', false);
|
||||
const defaultTimeRange = parseTimeQuery('Past 1h', false) as [Date, Date];
|
||||
export default function GraphPage() {
|
||||
const labelWidth = 350;
|
||||
|
||||
const [chartSeries, setChartSeries] = useHDXQueryParam<ChartSeries[]>(
|
||||
'series',
|
||||
[
|
||||
|
|
@ -58,112 +46,58 @@ export default function GraphPage() {
|
|||
},
|
||||
);
|
||||
|
||||
const setAggFn = (index: number, fn: AggFn) => {
|
||||
setChartSeries(
|
||||
produce(chartSeries, series => {
|
||||
const s = series?.[index];
|
||||
if (s != null && s.type === 'time') {
|
||||
s.aggFn = fn;
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
const setField = (index: number, field: string | undefined) => {
|
||||
setChartSeries(
|
||||
produce(chartSeries, series => {
|
||||
const s = series?.[index];
|
||||
if (s != null && s.type === 'time') {
|
||||
s.field = field;
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
const setFieldAndAggFn = (
|
||||
index: number,
|
||||
field: string | undefined,
|
||||
fn: AggFn,
|
||||
) => {
|
||||
setChartSeries(
|
||||
produce(chartSeries, series => {
|
||||
const s = series?.[index];
|
||||
if (s != null && s.type === 'time') {
|
||||
s.field = field;
|
||||
s.aggFn = fn;
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
const setWhere = (index: number, where: string) => {
|
||||
setChartSeries(
|
||||
produce(chartSeries, series => {
|
||||
const s = series?.[index];
|
||||
if (s != null && s.type === 'time') {
|
||||
s.where = where;
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
const setGroupBy = (index: number, groupBy: string | undefined) => {
|
||||
setChartSeries(
|
||||
produce(chartSeries, series => {
|
||||
const s = series?.[index];
|
||||
if (s != null && s.type === 'time') {
|
||||
s.groupBy = groupBy != null ? [groupBy] : [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const [granularity, setGranularity] = useQueryParam<
|
||||
| '30 second'
|
||||
| '1 minute'
|
||||
| '5 minute'
|
||||
| '10 minute'
|
||||
| '30 minute'
|
||||
| '1 hour'
|
||||
| '2 hour'
|
||||
| '12 hour'
|
||||
| '1 day'
|
||||
| '7 day'
|
||||
| undefined
|
||||
>('granularity', withDefault(StringParam, '5 minute') as any, {
|
||||
updateType: 'pushIn',
|
||||
const [granularity, setGranularity] = useHDXQueryParam<
|
||||
Granularity | undefined
|
||||
>('granularity', undefined, {
|
||||
queryParamConfig: {
|
||||
encode: (value: Granularity | undefined) => value ?? undefined,
|
||||
decode: (input: string | (string | null)[] | null | undefined) =>
|
||||
typeof input === 'string' && isGranularity(input) ? input : undefined,
|
||||
},
|
||||
});
|
||||
const [chartConfig, setChartConfig] = useState<any>();
|
||||
|
||||
const { displayedTimeInputValue, setDisplayedTimeInputValue, onSearch } =
|
||||
useTimeQuery({
|
||||
const [seriesReturnType, setSeriesReturnType] = useHDXQueryParam<
|
||||
'ratio' | 'column' | undefined
|
||||
>('seriesReturnType', undefined, {
|
||||
queryParamConfig: {
|
||||
encode: (value: 'ratio' | 'column' | undefined) => value ?? undefined,
|
||||
decode: (input: string | (string | null)[] | null | undefined) =>
|
||||
input === 'ratio' ? 'ratio' : 'column',
|
||||
},
|
||||
});
|
||||
|
||||
const editedChart = useMemo<Chart>(() => {
|
||||
return {
|
||||
id: 'chart-explorer',
|
||||
name: 'My New Chart',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 2,
|
||||
series: chartSeries,
|
||||
seriesReturnType: seriesReturnType ?? 'column',
|
||||
};
|
||||
}, [chartSeries, seriesReturnType]);
|
||||
|
||||
const setEditedChart = useCallback(
|
||||
(chart: Chart) => {
|
||||
setChartSeries(chart.series);
|
||||
setSeriesReturnType(chart.seriesReturnType);
|
||||
},
|
||||
[setChartSeries, setSeriesReturnType],
|
||||
);
|
||||
|
||||
const { isReady, searchedTimeRange, displayedTimeInputValue, onSearch } =
|
||||
useNewTimeQuery({
|
||||
isUTC: false,
|
||||
defaultValue: 'Past 1h',
|
||||
defaultTimeRange: [
|
||||
defaultTimeRange?.[0]?.getTime() ?? -1,
|
||||
defaultTimeRange?.[1]?.getTime() ?? -1,
|
||||
],
|
||||
initialDisplayValue: 'Past 1h',
|
||||
initialTimeRange: defaultTimeRange,
|
||||
});
|
||||
|
||||
const onRunQuery = useCallback(() => {
|
||||
onSearch(displayedTimeInputValue);
|
||||
const dateRange = parseTimeRangeInput(displayedTimeInputValue);
|
||||
|
||||
if (
|
||||
dateRange[0] != null &&
|
||||
dateRange[1] != null &&
|
||||
chartSeries[0].type === 'time'
|
||||
) {
|
||||
setChartConfig({
|
||||
// TODO: Support multiple series
|
||||
table: chartSeries[0].table ?? 'logs',
|
||||
aggFn: chartSeries[0].aggFn,
|
||||
field: chartSeries[0].field,
|
||||
where: chartSeries[0].where,
|
||||
groupBy: chartSeries[0].groupBy[0],
|
||||
granularity: granularity ?? '5 minute', // TODO: Auto granularity
|
||||
dateRange,
|
||||
});
|
||||
} else {
|
||||
toast.error('Invalid time range');
|
||||
}
|
||||
}, [chartSeries, displayedTimeInputValue, granularity, onSearch]);
|
||||
const [input, setInput] = useState<string>(displayedTimeInputValue);
|
||||
useEffect(() => {
|
||||
setInput(displayedTimeInputValue);
|
||||
}, [displayedTimeInputValue]);
|
||||
|
||||
return (
|
||||
<div className="LogViewerPage">
|
||||
|
|
@ -171,156 +105,26 @@ export default function GraphPage() {
|
|||
<title>Chart Explorer - HyperDX</title>
|
||||
</Head>
|
||||
<div
|
||||
style={{ background: '#16171D', height: '100vh' }}
|
||||
className="d-flex flex-column"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className="d-flex flex-column bg-hdx-dark p-3"
|
||||
>
|
||||
<form className="bg-body p-3" onSubmit={e => e.preventDefault()}>
|
||||
<div className="fs-5 mb-3 fw-500">Create New Chart</div>
|
||||
{chartSeries.map((series, index) => {
|
||||
if (series.type !== 'time') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ChartSeriesForm
|
||||
key={index}
|
||||
table={series.table}
|
||||
aggFn={series.aggFn}
|
||||
where={series.where}
|
||||
groupBy={series.groupBy[0]}
|
||||
field={series.field}
|
||||
setFieldAndAggFn={(field, aggFn) =>
|
||||
setFieldAndAggFn(index, field, aggFn)
|
||||
}
|
||||
setTableAndAggFn={(table, aggFn) => {
|
||||
setChartSeries(
|
||||
produce(chartSeries, series => {
|
||||
const s = series?.[index];
|
||||
if (s != null && s.type === 'time') {
|
||||
s.table = table;
|
||||
s.aggFn = aggFn;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}}
|
||||
setWhere={where => setWhere(index, where)}
|
||||
setAggFn={fn => setAggFn(index, fn)}
|
||||
setGroupBy={groupBy => setGroupBy(index, groupBy)}
|
||||
setField={field => setField(index, field)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="d-flex mt-3 align-items-center">
|
||||
<div
|
||||
style={{ width: labelWidth }}
|
||||
className="text-muted fw-500 ps-2"
|
||||
>
|
||||
Time Range
|
||||
</div>
|
||||
<div className="ms-3 flex-grow-1" style={{ maxWidth: 360 }}>
|
||||
<SearchTimeRangePicker
|
||||
inputValue={displayedTimeInputValue}
|
||||
setInputValue={setDisplayedTimeInputValue}
|
||||
onSearch={range => {
|
||||
setDisplayedTimeInputValue(range);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow-1 ms-3" style={{ maxWidth: 360 }}>
|
||||
<DSSelect
|
||||
options={[
|
||||
{
|
||||
value: '30 second' as const,
|
||||
label: '30 Seconds Granularity',
|
||||
},
|
||||
{
|
||||
value: '1 minute' as const,
|
||||
label: '1 Minute Granularity',
|
||||
},
|
||||
{
|
||||
value: '5 minute' as const,
|
||||
label: '5 Minutes Granularity',
|
||||
},
|
||||
{
|
||||
value: '10 minute' as const,
|
||||
label: '10 Minutes Granularity',
|
||||
},
|
||||
{
|
||||
value: '30 minute' as const,
|
||||
label: '30 Minutes Granularity',
|
||||
},
|
||||
{
|
||||
value: '1 hour' as const,
|
||||
label: '1 Hour Granularity',
|
||||
},
|
||||
{
|
||||
value: '12 hour' as const,
|
||||
label: '12 Hours Granularity',
|
||||
},
|
||||
{
|
||||
value: '1 day' as const,
|
||||
label: '1 Day Granularity',
|
||||
},
|
||||
{
|
||||
value: '7 day' as const,
|
||||
label: '7 Day Granularity',
|
||||
},
|
||||
]}
|
||||
onChange={setGranularity}
|
||||
value={granularity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ms-2 mt-3">
|
||||
<Button
|
||||
variant="outline-success"
|
||||
className="fs-7 text-muted-hover-black"
|
||||
onClick={onRunQuery}
|
||||
type="submit"
|
||||
>
|
||||
<i className="bi bi-graph-up me-2"></i>
|
||||
Run Query
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
className="w-100 mt-4 flex-grow-1"
|
||||
style={{ height: 400, minWidth: 0 }}
|
||||
>
|
||||
<ErrorBoundary
|
||||
onError={console.error}
|
||||
fallback={
|
||||
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent">
|
||||
An error occurred while rendering the chart.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{chartConfig != null && <HDXLineChart config={chartConfig} />}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
{chartConfig != null && chartConfig.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: 200 }} className="bg-hdx-dark">
|
||||
<LogTableWithSidePanel
|
||||
config={{
|
||||
...chartConfig,
|
||||
where: `${chartConfig.where} ${
|
||||
chartConfig.aggFn != 'count' && chartConfig.field != ''
|
||||
? `${chartConfig.field}:*`
|
||||
: ''
|
||||
} ${
|
||||
chartConfig.groupBy != '' && chartConfig.groupBy != null
|
||||
? `${chartConfig.groupBy}:*`
|
||||
: ''
|
||||
}`,
|
||||
}}
|
||||
isLive={false}
|
||||
isUTC={false}
|
||||
setIsUTC={() => {}}
|
||||
onPropertySearchClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isReady ? (
|
||||
<EditTileForm
|
||||
chart={editedChart}
|
||||
isLocalDashboard
|
||||
dateRange={searchedTimeRange}
|
||||
editedChart={editedChart}
|
||||
setEditedChart={setEditedChart}
|
||||
displayedTimeInputValue={input}
|
||||
setDisplayedTimeInputValue={setInput}
|
||||
onTimeRangeSearch={onSearch}
|
||||
granularity={granularity}
|
||||
setGranularity={setGranularity}
|
||||
hideSearch
|
||||
hideMarkdown
|
||||
/>
|
||||
) : (
|
||||
'Loading...'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ export enum Granularity {
|
|||
ThirtyDay = '30 day',
|
||||
}
|
||||
|
||||
export const isGranularity = (value: string): value is Granularity => {
|
||||
return Object.values(Granularity).includes(value as Granularity);
|
||||
};
|
||||
|
||||
const seriesDisplayName = (
|
||||
s: ChartSeries,
|
||||
{
|
||||
|
|
@ -172,7 +176,7 @@ export function seriesToSearchQuery({
|
|||
aggFn !== 'count' && field ? ` ${field}:*` : ''
|
||||
}${
|
||||
'groupBy' in s && s.groupBy != null && s.groupBy.length > 0
|
||||
? ` ${s.groupBy}:${groupByValue}`
|
||||
? ` ${s.groupBy}:${groupByValue ?? '*'}`
|
||||
: ''
|
||||
}`.trim();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,14 +40,7 @@ import {
|
|||
|
||||
import api from './api';
|
||||
import { convertDateRangeToGranularityString, Granularity } from './ChartUtils';
|
||||
import {
|
||||
EditHistogramChartForm,
|
||||
EditLineChartForm,
|
||||
EditMarkdownChartForm,
|
||||
EditNumberChartForm,
|
||||
EditSearchChartForm,
|
||||
EditTableChartForm,
|
||||
} from './EditChartForm';
|
||||
import EditTileForm from './EditTileForm';
|
||||
import GranularityPicker from './GranularityPicker';
|
||||
import HDXHistogramChart from './HDXHistogramChart';
|
||||
import HDXMarkdownChart from './HDXMarkdownChart';
|
||||
|
|
@ -59,8 +52,7 @@ import { withAppNav } from './layout';
|
|||
import { LogTableWithSidePanel } from './LogTableWithSidePanel';
|
||||
import SearchInput from './SearchInput';
|
||||
import SearchTimeRangePicker from './SearchTimeRangePicker';
|
||||
import { FloppyIcon, Histogram, TerraformFlatIcon } from './SVGIcons';
|
||||
import TabBar from './TabBar';
|
||||
import { FloppyIcon, TerraformFlatIcon } from './SVGIcons';
|
||||
import { Tags } from './Tags';
|
||||
import { parseTimeQuery, useNewTimeQuery } from './timeQuery';
|
||||
import type { Alert, Chart, Dashboard } from './types';
|
||||
|
|
@ -339,7 +331,7 @@ const Tile = forwardRef(
|
|||
},
|
||||
);
|
||||
|
||||
const EditChartModal = ({
|
||||
const EditTileModal = ({
|
||||
isLocalDashboard,
|
||||
chart,
|
||||
alerts,
|
||||
|
|
@ -356,25 +348,6 @@ const EditChartModal = ({
|
|||
onClose: () => void;
|
||||
show: boolean;
|
||||
}) => {
|
||||
type Tab =
|
||||
| 'time'
|
||||
| 'search'
|
||||
| 'histogram'
|
||||
| 'markdown'
|
||||
| 'number'
|
||||
| 'table'
|
||||
| undefined;
|
||||
|
||||
const [tab, setTab] = useState<Tab>(undefined);
|
||||
const displayedTab = tab ?? chart?.series?.[0]?.type ?? 'time';
|
||||
|
||||
const onTabClick = useCallback(
|
||||
(newTab: Tab) => {
|
||||
setTab(newTab);
|
||||
},
|
||||
[setTab],
|
||||
);
|
||||
|
||||
return (
|
||||
<ZIndexContext.Provider value={1055}>
|
||||
<Modal
|
||||
|
|
@ -385,127 +358,18 @@ const EditChartModal = ({
|
|||
size="xl"
|
||||
enforceFocus={false}
|
||||
>
|
||||
<Modal.Body className="bg-hdx-dark rounded">
|
||||
<TabBar
|
||||
className="fs-8 mb-3"
|
||||
items={[
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-graph-up" /> Line Chart
|
||||
</span>
|
||||
),
|
||||
value: 'time',
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-card-list" /> Search Results
|
||||
</span>
|
||||
),
|
||||
value: 'search',
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-table" /> Table
|
||||
</span>
|
||||
),
|
||||
value: 'table',
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<Histogram width={12} color="#fff" /> Histogram
|
||||
</span>
|
||||
),
|
||||
value: 'histogram',
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-123"></i> Number
|
||||
</span>
|
||||
),
|
||||
value: 'number',
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-markdown"></i> Markdown
|
||||
</span>
|
||||
),
|
||||
value: 'markdown',
|
||||
},
|
||||
]}
|
||||
activeItem={displayedTab}
|
||||
onClick={onTabClick}
|
||||
<Modal.Body
|
||||
className="bg-hdx-dark rounded d-flex flex-column"
|
||||
style={{ minHeight: '80vh' }}
|
||||
>
|
||||
<EditTileForm
|
||||
isLocalDashboard={isLocalDashboard}
|
||||
chart={chart}
|
||||
alerts={alerts}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
{displayedTab === 'time' && chart != null && (
|
||||
<EditLineChartForm
|
||||
isLocalDashboard={isLocalDashboard}
|
||||
chart={produce(chart, draft => {
|
||||
for (const series of draft.series) {
|
||||
series.type = 'time';
|
||||
}
|
||||
})}
|
||||
alerts={alerts}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'table' && chart != null && (
|
||||
<EditTableChartForm
|
||||
chart={produce(chart, draft => {
|
||||
for (const series of draft.series) {
|
||||
series.type = 'table';
|
||||
}
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'histogram' && chart != null && (
|
||||
<EditHistogramChartForm
|
||||
chart={produce(chart, draft => {
|
||||
draft.series[0].type = 'histogram';
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'search' && chart != null && (
|
||||
<EditSearchChartForm
|
||||
chart={produce(chart, draft => {
|
||||
draft.series[0].type = 'search';
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'number' && chart != null && (
|
||||
<EditNumberChartForm
|
||||
chart={produce(chart, draft => {
|
||||
draft.series[0].type = 'number';
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'markdown' && chart != null && (
|
||||
<EditMarkdownChartForm
|
||||
chart={produce(chart, draft => {
|
||||
draft.series[0].type = 'markdown';
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</ZIndexContext.Provider>
|
||||
|
|
@ -955,7 +819,7 @@ export default function DashboardPage() {
|
|||
<title>Dashboard - HyperDX</title>
|
||||
</Head>
|
||||
{dashboard != null ? (
|
||||
<EditChartModal
|
||||
<EditTileModal
|
||||
isLocalDashboard={isLocalDashboard}
|
||||
dateRange={searchedTimeRange}
|
||||
key={editedChart?.id}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
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';
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
ChartSeriesFormCompact,
|
||||
convertDateRangeToGranularityString,
|
||||
FieldSelect,
|
||||
Granularity,
|
||||
GroupBySelect,
|
||||
seriesToSearchQuery,
|
||||
TableSelect,
|
||||
|
|
@ -20,13 +21,15 @@ import Checkbox from './Checkbox';
|
|||
import * as config from './config';
|
||||
import { METRIC_ALERTS_ENABLED } 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 type { Alert, Chart, TimeChartSeries } from './types';
|
||||
import SearchTimeRangePicker from './SearchTimeRangePicker';
|
||||
import type { Alert, Chart, ChartSeries, TimeChartSeries } from './types';
|
||||
import { useDebounce } from './utils';
|
||||
|
||||
const DEFAULT_ALERT: Alert = {
|
||||
|
|
@ -45,8 +48,8 @@ export const EditMarkdownChartForm = ({
|
|||
onSave,
|
||||
}: {
|
||||
chart: Chart | undefined;
|
||||
onSave: (chart: Chart) => void;
|
||||
onClose: () => void;
|
||||
onSave?: (chart: Chart) => void;
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
|
||||
|
||||
|
|
@ -74,7 +77,7 @@ export const EditMarkdownChartForm = ({
|
|||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
onSave(editedChart);
|
||||
onSave?.(editedChart);
|
||||
}}
|
||||
>
|
||||
<div className="fs-5 mb-4">Markdown</div>
|
||||
|
|
@ -118,18 +121,24 @@ export const EditMarkdownChartForm = ({
|
|||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between my-3 ps-2">
|
||||
<BSButton
|
||||
variant="outline-success"
|
||||
className="fs-7 text-muted-hover-black"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</BSButton>
|
||||
<BSButton onClick={onClose} variant="dark">
|
||||
Cancel
|
||||
</BSButton>
|
||||
</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">
|
||||
|
|
@ -148,8 +157,8 @@ export const EditSearchChartForm = ({
|
|||
}: {
|
||||
chart: Chart | undefined;
|
||||
dateRange: [Date, Date];
|
||||
onSave: (chart: Chart) => void;
|
||||
onClose: () => void;
|
||||
onSave?: (chart: Chart) => void;
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
|
||||
|
||||
|
|
@ -178,7 +187,7 @@ export const EditSearchChartForm = ({
|
|||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
onSave(editedChart);
|
||||
onSave?.(editedChart);
|
||||
}}
|
||||
>
|
||||
<div className="fs-5 mb-4">Search Builder</div>
|
||||
|
|
@ -221,18 +230,24 @@ export const EditSearchChartForm = ({
|
|||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between my-3 ps-2">
|
||||
<BSButton
|
||||
variant="outline-success"
|
||||
className="fs-7 text-muted-hover-black"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</BSButton>
|
||||
<BSButton onClick={onClose} variant="dark">
|
||||
Cancel
|
||||
</BSButton>
|
||||
</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</div>
|
||||
<div style={{ height: 400 }} className="bg-hdx-dark">
|
||||
|
|
@ -257,45 +272,61 @@ export const EditNumberChartForm = ({
|
|||
onClose,
|
||||
onSave,
|
||||
dateRange,
|
||||
editedChart,
|
||||
setEditedChart,
|
||||
displayedTimeInputValue,
|
||||
setDisplayedTimeInputValue,
|
||||
onTimeRangeSearch,
|
||||
}: {
|
||||
chart: Chart | undefined;
|
||||
dateRange: [Date, Date];
|
||||
onSave: (chart: Chart) => void;
|
||||
onClose: () => void;
|
||||
onSave?: (chart: Chart) => void;
|
||||
onClose?: () => void;
|
||||
displayedTimeInputValue?: string;
|
||||
setDisplayedTimeInputValue?: (value: string) => void;
|
||||
onTimeRangeSearch?: (value: string) => void;
|
||||
editedChart?: Chart;
|
||||
setEditedChart?: (chart: Chart) => void;
|
||||
}) => {
|
||||
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
|
||||
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'
|
||||
return _editedChart != null && _editedChart.series[0].type === 'number'
|
||||
? {
|
||||
aggFn: editedChart.series[0].aggFn ?? 'count',
|
||||
table: editedChart.series[0].table ?? 'logs',
|
||||
field: editedChart.series[0].field ?? '', // TODO: Fix in definition
|
||||
where: editedChart.series[0].where,
|
||||
aggFn: _editedChart.series[0].aggFn ?? 'count',
|
||||
table: _editedChart.series[0].table ?? 'logs',
|
||||
field: _editedChart.series[0].field ?? '', // TODO: Fix in definition
|
||||
where: _editedChart.series[0].where,
|
||||
dateRange,
|
||||
numberFormat: editedChart.series[0].numberFormat,
|
||||
numberFormat: _editedChart.series[0].numberFormat,
|
||||
}
|
||||
: null;
|
||||
}, [editedChart, dateRange]);
|
||||
}, [_editedChart, dateRange]);
|
||||
const previewConfig = useDebounce(chartConfig, 500);
|
||||
|
||||
if (
|
||||
chartConfig == null ||
|
||||
editedChart == null ||
|
||||
_editedChart == null ||
|
||||
previewConfig == null ||
|
||||
editedChart.series[0].type !== 'number'
|
||||
_editedChart.series[0].type !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelWidth = 320;
|
||||
const aggFn = editedChart.series[0].aggFn ?? 'count';
|
||||
const aggFn = _editedChart.series[0].aggFn ?? 'count';
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
onSave(editedChart);
|
||||
onSave?.(_editedChart);
|
||||
}}
|
||||
>
|
||||
<div className="fs-5 mb-4">Number Tile Builder</div>
|
||||
|
|
@ -304,13 +335,13 @@ export const EditNumberChartForm = ({
|
|||
type="text"
|
||||
id="name"
|
||||
onChange={e =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
draft.name = e.target.value;
|
||||
}),
|
||||
)
|
||||
}
|
||||
defaultValue={editedChart.name}
|
||||
defaultValue={_editedChart.name}
|
||||
placeholder="Chart Name"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -324,8 +355,8 @@ export const EditNumberChartForm = ({
|
|||
className="ds-select"
|
||||
value={AGG_FNS.find(v => v.value === aggFn)}
|
||||
onChange={opt => {
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
if (draft.series[0].type === 'number') {
|
||||
draft.series[0].aggFn = opt?.value ?? 'count';
|
||||
}
|
||||
|
|
@ -343,10 +374,10 @@ export const EditNumberChartForm = ({
|
|||
</div>
|
||||
<div className="ms-3 flex-grow-1">
|
||||
<FieldSelect
|
||||
value={editedChart.series[0].field ?? ''}
|
||||
value={_editedChart.series[0].field ?? ''}
|
||||
setValue={field =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
if (draft.series[0].type === 'number') {
|
||||
draft.series[0].field = field;
|
||||
}
|
||||
|
|
@ -372,10 +403,10 @@ export const EditNumberChartForm = ({
|
|||
type="text"
|
||||
placeholder={'Filter results by a search query'}
|
||||
className="border-0 fs-7"
|
||||
value={editedChart.series[0].where}
|
||||
value={_editedChart.series[0].where}
|
||||
onChange={event =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
if (draft.series[0].type === 'number') {
|
||||
draft.series[0].where = event.target.value;
|
||||
}
|
||||
|
|
@ -400,10 +431,10 @@ export const EditNumberChartForm = ({
|
|||
<Group>
|
||||
<div className="fs-8 text-slate-300">Number Format</div>
|
||||
<NumberFormatInput
|
||||
value={editedChart.series[0].numberFormat}
|
||||
value={_editedChart.series[0].numberFormat}
|
||||
onChange={numberFormat =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
if (draft.series[0].type === 'number') {
|
||||
draft.series[0].numberFormat = numberFormat;
|
||||
}
|
||||
|
|
@ -413,26 +444,48 @@ export const EditNumberChartForm = ({
|
|||
/>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between my-3 ps-2">
|
||||
<BSButton
|
||||
variant="outline-success"
|
||||
className="fs-7 text-muted-hover-black"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</BSButton>
|
||||
<BSButton onClick={onClose} variant="dark">
|
||||
Cancel
|
||||
</BSButton>
|
||||
</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">Chart Preview</div>
|
||||
<Flex justify="space-between" align="center" mb="sm">
|
||||
<div className="text-muted ps-2 fs-7" style={{ flexGrow: 1 }}>
|
||||
Chart Preview
|
||||
</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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
<div style={{ height: 400 }}>
|
||||
<HDXNumberChart config={previewConfig} />
|
||||
</div>
|
||||
</div>
|
||||
{editedChart.series[0].table === 'logs' ? (
|
||||
{_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>
|
||||
|
|
@ -462,51 +515,68 @@ export const EditTableChartForm = ({
|
|||
onClose,
|
||||
onSave,
|
||||
dateRange,
|
||||
displayedTimeInputValue,
|
||||
setDisplayedTimeInputValue,
|
||||
onTimeRangeSearch,
|
||||
editedChart,
|
||||
setEditedChart,
|
||||
}: {
|
||||
chart: Chart | undefined;
|
||||
dateRange: [Date, Date];
|
||||
onSave: (chart: Chart) => void;
|
||||
onClose: () => void;
|
||||
onSave?: (chart: Chart) => void;
|
||||
onClose?: () => void;
|
||||
editedChart?: Chart;
|
||||
setEditedChart?: (chart: Chart) => void;
|
||||
displayedTimeInputValue?: string;
|
||||
setDisplayedTimeInputValue?: (value: string) => void;
|
||||
onTimeRangeSearch?: (value: string) => void;
|
||||
}) => {
|
||||
const CHART_TYPE = 'table';
|
||||
|
||||
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
|
||||
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
|
||||
_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',
|
||||
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,
|
||||
numberFormat: _editedChart.series[0].numberFormat,
|
||||
series: _editedChart.series,
|
||||
seriesReturnType: _editedChart.seriesReturnType,
|
||||
}
|
||||
: null,
|
||||
[editedChart, dateRange],
|
||||
[_editedChart, dateRange],
|
||||
);
|
||||
const previewConfig = useDebounce(chartConfig, 500);
|
||||
|
||||
if (
|
||||
chartConfig == null ||
|
||||
previewConfig == null ||
|
||||
editedChart == null ||
|
||||
editedChart.series[0].type !== CHART_TYPE
|
||||
_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);
|
||||
onSave?.(_editedChart);
|
||||
}}
|
||||
>
|
||||
<div className="fs-5 mb-4">Table Builder</div>
|
||||
|
|
@ -515,67 +585,95 @@ export const EditTableChartForm = ({
|
|||
type="text"
|
||||
id="name"
|
||||
onChange={e =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
draft.name = e.target.value;
|
||||
}),
|
||||
)
|
||||
}
|
||||
defaultValue={editedChart.name}
|
||||
defaultValue={_editedChart.name}
|
||||
placeholder="Chart Name"
|
||||
/>
|
||||
</div>
|
||||
<EditMultiSeriesChartForm
|
||||
{...{ editedChart, setEditedChart, CHART_TYPE }}
|
||||
{...{
|
||||
editedChart: _editedChart,
|
||||
setEditedChart: _setEditedChart,
|
||||
CHART_TYPE,
|
||||
}}
|
||||
/>
|
||||
<div className="d-flex justify-content-between my-3 ps-2">
|
||||
<BSButton
|
||||
variant="outline-success"
|
||||
className="fs-7 text-muted-hover-black"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</BSButton>
|
||||
<BSButton onClick={onClose} variant="dark">
|
||||
Cancel
|
||||
</BSButton>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="mb-3 text-muted ps-2 fs-7">Chart Preview</div>
|
||||
<div style={{ height: 400 }}>
|
||||
<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;
|
||||
}
|
||||
{(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
|
||||
</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);
|
||||
}}
|
||||
/>
|
||||
</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';
|
||||
}
|
||||
const s = draft.series[seriesIndex];
|
||||
if (s.type === CHART_TYPE) {
|
||||
s.sortOrder =
|
||||
s.sortOrder == null
|
||||
? 'desc'
|
||||
: s.sortOrder === 'asc'
|
||||
? 'desc'
|
||||
: 'asc';
|
||||
}
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{editedChart.series[0].table === 'logs' ? (
|
||||
{_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>
|
||||
|
|
@ -611,31 +709,47 @@ export const EditHistogramChartForm = ({
|
|||
onClose,
|
||||
onSave,
|
||||
dateRange,
|
||||
displayedTimeInputValue,
|
||||
setDisplayedTimeInputValue,
|
||||
onTimeRangeSearch,
|
||||
editedChart,
|
||||
setEditedChart,
|
||||
}: {
|
||||
chart: Chart | undefined;
|
||||
dateRange: [Date, Date];
|
||||
onSave: (chart: Chart) => void;
|
||||
onClose: () => void;
|
||||
onSave?: (chart: Chart) => void;
|
||||
onClose?: () => void;
|
||||
editedChart?: Chart;
|
||||
setEditedChart?: (chart: Chart) => void;
|
||||
displayedTimeInputValue?: string;
|
||||
setDisplayedTimeInputValue?: (value: string) => void;
|
||||
onTimeRangeSearch?: (value: string) => void;
|
||||
}) => {
|
||||
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
|
||||
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'
|
||||
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,
|
||||
table: _editedChart.series[0].table ?? 'logs',
|
||||
field: _editedChart.series[0].field ?? '', // TODO: Fix in definition
|
||||
where: _editedChart.series[0].where,
|
||||
dateRange,
|
||||
}
|
||||
: null;
|
||||
}, [editedChart, dateRange]);
|
||||
}, [_editedChart, dateRange]);
|
||||
const previewConfig = useDebounce(chartConfig, 500);
|
||||
|
||||
if (
|
||||
chartConfig == null ||
|
||||
editedChart == null ||
|
||||
_editedChart == null ||
|
||||
previewConfig == null ||
|
||||
editedChart.series[0].type !== 'histogram'
|
||||
_editedChart.series[0].type !== 'histogram'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -646,7 +760,7 @@ export const EditHistogramChartForm = ({
|
|||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
onSave(editedChart);
|
||||
onSave?.(_editedChart);
|
||||
}}
|
||||
>
|
||||
<div className="fs-5 mb-4">Histogram Builder</div>
|
||||
|
|
@ -655,13 +769,13 @@ export const EditHistogramChartForm = ({
|
|||
type="text"
|
||||
id="name"
|
||||
onChange={e =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
draft.name = e.target.value;
|
||||
}),
|
||||
)
|
||||
}
|
||||
defaultValue={editedChart.name}
|
||||
defaultValue={_editedChart.name}
|
||||
placeholder="Chart Name"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -671,10 +785,10 @@ export const EditHistogramChartForm = ({
|
|||
</div>
|
||||
<div className="ms-3 flex-grow-1">
|
||||
<FieldSelect
|
||||
value={editedChart.series[0].field ?? ''}
|
||||
value={_editedChart.series[0].field ?? ''}
|
||||
setValue={field =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
if (draft.series[0].type === 'histogram') {
|
||||
draft.series[0].field = field;
|
||||
}
|
||||
|
|
@ -695,10 +809,10 @@ export const EditHistogramChartForm = ({
|
|||
type="text"
|
||||
placeholder={'Filter results by a search query'}
|
||||
className="border-0 fs-7"
|
||||
value={editedChart.series[0].where}
|
||||
value={_editedChart.series[0].where}
|
||||
onChange={event =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
if (draft.series[0].type === 'histogram') {
|
||||
draft.series[0].where = event.target.value;
|
||||
}
|
||||
|
|
@ -709,25 +823,48 @@ export const EditHistogramChartForm = ({
|
|||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between my-3 ps-2">
|
||||
<BSButton
|
||||
variant="outline-success"
|
||||
className="fs-7 text-muted-hover-black"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</BSButton>
|
||||
<BSButton onClick={onClose} variant="dark">
|
||||
Cancel
|
||||
</BSButton>
|
||||
</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">Chart Preview</div>
|
||||
<Flex justify="space-between" align="center" mb="sm">
|
||||
<div className="text-muted ps-2 fs-7" style={{ flexGrow: 1 }}>
|
||||
Chart Preview
|
||||
</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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
<div style={{ height: 400 }}>
|
||||
<HDXHistogramChart config={previewConfig} />
|
||||
</div>
|
||||
</div>
|
||||
{editedChart.series[0].table === 'logs' ? (
|
||||
{_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>
|
||||
|
|
@ -1038,61 +1175,85 @@ export const EditLineChartForm = ({
|
|||
onClose,
|
||||
onSave,
|
||||
dateRange,
|
||||
editedChart,
|
||||
setEditedChart,
|
||||
granularity,
|
||||
setGranularity,
|
||||
setDisplayedTimeInputValue,
|
||||
displayedTimeInputValue,
|
||||
onTimeRangeSearch,
|
||||
}: {
|
||||
isLocalDashboard: boolean;
|
||||
chart: Chart | undefined;
|
||||
alerts: Alert[];
|
||||
alerts?: Alert[];
|
||||
dateRange: [Date, Date];
|
||||
onSave: (chart: Chart, alerts?: Alert[]) => void;
|
||||
onClose: () => void;
|
||||
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;
|
||||
}) => {
|
||||
const CHART_TYPE = 'time';
|
||||
const [alert] = alerts; // TODO: Support multiple alerts eventually
|
||||
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
|
||||
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);
|
||||
|
||||
const [_editedChart, _setEditedChart] =
|
||||
editedChart != null && setEditedChart != null
|
||||
? [editedChart, setEditedChart]
|
||||
: [editedChartState, setEditedChartState];
|
||||
|
||||
const chartConfig = useMemo(
|
||||
() =>
|
||||
editedChart != null && editedChart.series?.[0]?.type === CHART_TYPE
|
||||
_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,
|
||||
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)
|
||||
: convertDateRangeToGranularityString(dateRange, 60),
|
||||
: granularity ??
|
||||
convertDateRangeToGranularityString(dateRange, 60),
|
||||
dateRange,
|
||||
numberFormat: editedChart.series[0].numberFormat,
|
||||
series: editedChart.series,
|
||||
seriesReturnType: editedChart.seriesReturnType,
|
||||
numberFormat: _editedChart.series[0].numberFormat,
|
||||
series: _editedChart.series,
|
||||
seriesReturnType: _editedChart.seriesReturnType,
|
||||
}
|
||||
: null,
|
||||
[editedChart, alertEnabled, editedAlert?.interval, dateRange],
|
||||
[_editedChart, alertEnabled, editedAlert?.interval, dateRange, granularity],
|
||||
);
|
||||
const previewConfig = useDebounce(chartConfig, 500);
|
||||
|
||||
if (
|
||||
chartConfig == null ||
|
||||
previewConfig == null ||
|
||||
editedChart == null ||
|
||||
editedChart.series[0].type !== 'time'
|
||||
_editedChart == null ||
|
||||
_editedChart.series[0].type !== 'time'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isChartAlertsFeatureEnabled =
|
||||
editedChart.series[0].table === 'logs' || METRIC_ALERTS_ENABLED;
|
||||
alerts != null &&
|
||||
(_editedChart.series[0].table === 'logs' || METRIC_ALERTS_ENABLED);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="d-flex flex-column flex-grow-1"
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
onSave(
|
||||
editedChart,
|
||||
onSave?.(
|
||||
_editedChart,
|
||||
alertEnabled ? [editedAlert ?? DEFAULT_ALERT] : undefined,
|
||||
);
|
||||
}}
|
||||
|
|
@ -1103,18 +1264,22 @@ export const EditLineChartForm = ({
|
|||
type="text"
|
||||
id="name"
|
||||
onChange={e =>
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
_setEditedChart(
|
||||
produce(_editedChart, draft => {
|
||||
draft.name = e.target.value;
|
||||
}),
|
||||
)
|
||||
}
|
||||
defaultValue={editedChart.name}
|
||||
defaultValue={_editedChart.name}
|
||||
placeholder="Chart Name"
|
||||
/>
|
||||
</div>
|
||||
<EditMultiSeriesChartForm
|
||||
{...{ editedChart, setEditedChart, CHART_TYPE }}
|
||||
{...{
|
||||
editedChart: _editedChart,
|
||||
setEditedChart: _setEditedChart,
|
||||
CHART_TYPE,
|
||||
}}
|
||||
/>
|
||||
|
||||
{isChartAlertsFeatureEnabled && (
|
||||
|
|
@ -1137,7 +1302,7 @@ export const EditLineChartForm = ({
|
|||
<EditChartFormAlerts
|
||||
alert={editedAlert ?? DEFAULT_ALERT}
|
||||
setAlert={setEditedAlert}
|
||||
numberFormat={editedChart.series[0].numberFormat}
|
||||
numberFormat={_editedChart.series[0].numberFormat}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1146,32 +1311,67 @@ export const EditLineChartForm = ({
|
|||
</Paper>
|
||||
)}
|
||||
|
||||
<div className="d-flex justify-content-between my-3">
|
||||
<BSButton
|
||||
variant="outline-success"
|
||||
className="fs-7 text-muted-hover-black"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</BSButton>
|
||||
<BSButton onClick={onClose} variant="dark">
|
||||
Cancel
|
||||
</BSButton>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="mb-3 text-muted ps-2 fs-7">Chart Preview</div>
|
||||
<div style={{ height: 400 }}>
|
||||
<HDXMultiSeriesTimeChart
|
||||
config={previewConfig}
|
||||
{...(alertEnabled && {
|
||||
alertThreshold: editedAlert?.threshold,
|
||||
alertThresholdType:
|
||||
editedAlert?.type === 'presence' ? 'above' : 'below',
|
||||
})}
|
||||
/>
|
||||
{(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
|
||||
</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);
|
||||
}}
|
||||
/>
|
||||
</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' ? (
|
||||
{_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>
|
||||
|
|
|
|||
234
packages/app/src/EditTileForm.tsx
Normal file
234
packages/app/src/EditTileForm.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import produce from 'immer';
|
||||
|
||||
import { Granularity } from './ChartUtils';
|
||||
import {
|
||||
EditHistogramChartForm,
|
||||
EditLineChartForm,
|
||||
EditMarkdownChartForm,
|
||||
EditNumberChartForm,
|
||||
EditSearchChartForm,
|
||||
EditTableChartForm,
|
||||
} from './EditChartForm';
|
||||
import { Histogram } from './SVGIcons';
|
||||
import TabBar from './TabBar';
|
||||
import type { Alert, Chart, Dashboard } from './types';
|
||||
|
||||
const EditTileForm = ({
|
||||
isLocalDashboard,
|
||||
chart,
|
||||
alerts,
|
||||
dateRange,
|
||||
onSave,
|
||||
onClose,
|
||||
editedChart,
|
||||
setEditedChart,
|
||||
displayedTimeInputValue,
|
||||
setDisplayedTimeInputValue,
|
||||
granularity,
|
||||
setGranularity,
|
||||
onTimeRangeSearch,
|
||||
hideMarkdown,
|
||||
hideSearch,
|
||||
}: {
|
||||
isLocalDashboard: boolean;
|
||||
chart: Chart | undefined;
|
||||
alerts?: Alert[];
|
||||
dateRange: [Date, Date];
|
||||
displayedTimeInputValue?: string;
|
||||
setDisplayedTimeInputValue?: (value: string) => void;
|
||||
onTimeRangeSearch?: (value: string) => void;
|
||||
granularity?: Granularity;
|
||||
setGranularity?: (granularity: Granularity | undefined) => void;
|
||||
onSave?: (chart: Chart, alerts?: Alert[]) => void;
|
||||
onClose?: () => void;
|
||||
editedChart?: Chart;
|
||||
setEditedChart?: (chart: Chart) => void;
|
||||
hideMarkdown?: boolean;
|
||||
hideSearch?: boolean;
|
||||
}) => {
|
||||
type Tab =
|
||||
| 'time'
|
||||
| 'search'
|
||||
| 'histogram'
|
||||
| 'markdown'
|
||||
| 'number'
|
||||
| 'table'
|
||||
| undefined;
|
||||
|
||||
const [tab, setTab] = useState<Tab>(undefined);
|
||||
const displayedTab = tab ?? chart?.series?.[0]?.type ?? 'time';
|
||||
|
||||
const onTabClick = useCallback(
|
||||
(newTab: Tab) => {
|
||||
setTab(newTab);
|
||||
if (setEditedChart != null && editedChart != null) {
|
||||
setEditedChart(
|
||||
produce(editedChart, draft => {
|
||||
for (const series of draft.series) {
|
||||
series.type = newTab ?? 'time';
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
[setTab, setEditedChart, editedChart],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabBar
|
||||
className="fs-8 mb-3"
|
||||
items={[
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-graph-up" /> Line Chart
|
||||
</span>
|
||||
),
|
||||
value: 'time',
|
||||
},
|
||||
...(hideSearch === true
|
||||
? []
|
||||
: [
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-card-list" /> Search Results
|
||||
</span>
|
||||
),
|
||||
value: 'search' as const,
|
||||
},
|
||||
]),
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-table" /> Table
|
||||
</span>
|
||||
),
|
||||
value: 'table',
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<Histogram width={12} color="#fff" /> Histogram
|
||||
</span>
|
||||
),
|
||||
value: 'histogram',
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-123"></i> Number
|
||||
</span>
|
||||
),
|
||||
value: 'number',
|
||||
},
|
||||
...(hideMarkdown === true
|
||||
? []
|
||||
: [
|
||||
{
|
||||
text: (
|
||||
<span>
|
||||
<i className="bi bi-markdown"></i> Markdown
|
||||
</span>
|
||||
),
|
||||
value: 'markdown' as const,
|
||||
},
|
||||
]),
|
||||
]}
|
||||
activeItem={displayedTab}
|
||||
onClick={onTabClick}
|
||||
/>
|
||||
{displayedTab === 'time' && chart != null && (
|
||||
<EditLineChartForm
|
||||
isLocalDashboard={isLocalDashboard}
|
||||
chart={produce(chart, draft => {
|
||||
for (const series of draft.series) {
|
||||
series.type = 'time';
|
||||
}
|
||||
})}
|
||||
alerts={alerts}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
editedChart={editedChart}
|
||||
setEditedChart={setEditedChart}
|
||||
setDisplayedTimeInputValue={setDisplayedTimeInputValue}
|
||||
displayedTimeInputValue={displayedTimeInputValue}
|
||||
onTimeRangeSearch={onTimeRangeSearch}
|
||||
granularity={granularity}
|
||||
setGranularity={setGranularity}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'table' && chart != null && (
|
||||
<EditTableChartForm
|
||||
chart={produce(chart, draft => {
|
||||
for (const series of draft.series) {
|
||||
series.type = 'table';
|
||||
}
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
editedChart={editedChart}
|
||||
setEditedChart={setEditedChart}
|
||||
setDisplayedTimeInputValue={setDisplayedTimeInputValue}
|
||||
displayedTimeInputValue={displayedTimeInputValue}
|
||||
onTimeRangeSearch={onTimeRangeSearch}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'histogram' && chart != null && (
|
||||
<EditHistogramChartForm
|
||||
chart={produce(chart, draft => {
|
||||
draft.series[0].type = 'histogram';
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
editedChart={editedChart}
|
||||
setEditedChart={setEditedChart}
|
||||
setDisplayedTimeInputValue={setDisplayedTimeInputValue}
|
||||
displayedTimeInputValue={displayedTimeInputValue}
|
||||
onTimeRangeSearch={onTimeRangeSearch}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'search' && chart != null && (
|
||||
<EditSearchChartForm
|
||||
chart={produce(chart, draft => {
|
||||
draft.series[0].type = 'search';
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'number' && chart != null && (
|
||||
<EditNumberChartForm
|
||||
chart={produce(chart, draft => {
|
||||
draft.series[0].type = 'number';
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
editedChart={editedChart}
|
||||
setEditedChart={setEditedChart}
|
||||
dateRange={dateRange}
|
||||
setDisplayedTimeInputValue={setDisplayedTimeInputValue}
|
||||
displayedTimeInputValue={displayedTimeInputValue}
|
||||
onTimeRangeSearch={onTimeRangeSearch}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === 'markdown' && chart != null && (
|
||||
<EditMarkdownChartForm
|
||||
chart={produce(chart, draft => {
|
||||
draft.series[0].type = 'markdown';
|
||||
})}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTileForm;
|
||||
|
|
@ -353,7 +353,7 @@ const HDXMultiSeriesTableChart = memo(
|
|||
No data found within time range.
|
||||
</div>
|
||||
) : (
|
||||
<div className="d-flex align-items-center justify-content-center fs-2 h-100">
|
||||
<div className="d-flex fs-2 h-100 flex-grow-1">
|
||||
<Table
|
||||
data={data?.data ?? []}
|
||||
groupColumnName={
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const MemoChart = memo(function MemoChart({
|
|||
graphResults: any[];
|
||||
setIsClickActive: (v: any) => void;
|
||||
isClickActive: any;
|
||||
dateRange: [Date, Date];
|
||||
dateRange: [Date, Date] | Readonly<[Date, Date]>;
|
||||
groupKeys: string[];
|
||||
lineNames: string[];
|
||||
alertThreshold?: number;
|
||||
|
|
@ -228,7 +228,7 @@ const HDXMultiSeriesTimeChart = memo(
|
|||
config: {
|
||||
series: ChartSeries[];
|
||||
granularity: Granularity;
|
||||
dateRange: [Date, Date];
|
||||
dateRange: [Date, Date] | Readonly<[Date, Date]>;
|
||||
seriesReturnType: 'ratio' | 'column';
|
||||
};
|
||||
onSettled?: () => void;
|
||||
|
|
@ -386,6 +386,7 @@ const HDXMultiSeriesTimeChart = memo(
|
|||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
Loading…
Reference in a new issue