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:
Mike Shi 2024-01-31 16:31:12 -08:00 committed by GitHub
parent 3fc4d52bf9
commit 95ccfa1a51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 759 additions and 646 deletions

View file

@ -0,0 +1,6 @@
---
'@hyperdx/app': patch
---
Add multi-series line/table charts as well as histogram/number charts to the
chart explorer.

View file

@ -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>

View file

@ -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();
}

View file

@ -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}

View file

@ -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>

View 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;

View file

@ -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={

View file

@ -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