mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui/vmanomaly: integrate vmanomaly query_server (#6017)
* vmui: fix parsing of fractional values * vmui/vmanomaly: update display logic to align with vmanomaly /query_range API * vmui/vmanomaly: rename flag anomalyView to isAnomalyView
This commit is contained in:
parent
22497c2c98
commit
f06f55edb6
22 changed files with 308 additions and 474 deletions
|
@ -1,3 +1,3 @@
|
||||||
## Predefined dashboards
|
## Predefined dashboards
|
||||||
|
|
||||||
See [this docs](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui#predefined-dashboards)
|
See [this doc](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui#predefined-dashboards)
|
||||||
|
|
|
@ -7,10 +7,11 @@ import "./style.scss";
|
||||||
interface LegendProps {
|
interface LegendProps {
|
||||||
labels: LegendItemType[];
|
labels: LegendItemType[];
|
||||||
query: string[];
|
query: string[];
|
||||||
|
isAnomalyView?: boolean;
|
||||||
onChange: (item: LegendItemType, metaKey: boolean) => void;
|
onChange: (item: LegendItemType, metaKey: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Legend: FC<LegendProps> = ({ labels, query, onChange }) => {
|
const Legend: FC<LegendProps> = ({ labels, query, isAnomalyView, onChange }) => {
|
||||||
const groups = useMemo(() => {
|
const groups = useMemo(() => {
|
||||||
return Array.from(new Set(labels.map(l => l.group)));
|
return Array.from(new Set(labels.map(l => l.group)));
|
||||||
}, [labels]);
|
}, [labels]);
|
||||||
|
@ -39,6 +40,7 @@ const Legend: FC<LegendProps> = ({ labels, query, onChange }) => {
|
||||||
<LegendItem
|
<LegendItem
|
||||||
key={legendItem.label}
|
key={legendItem.label}
|
||||||
legend={legendItem}
|
legend={legendItem}
|
||||||
|
isAnomalyView={isAnomalyView}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -11,9 +11,10 @@ interface LegendItemProps {
|
||||||
legend: LegendItemType;
|
legend: LegendItemType;
|
||||||
onChange?: (item: LegendItemType, metaKey: boolean) => void;
|
onChange?: (item: LegendItemType, metaKey: boolean) => void;
|
||||||
isHeatmap?: boolean;
|
isHeatmap?: boolean;
|
||||||
|
isAnomalyView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap, isAnomalyView }) => {
|
||||||
const copyToClipboard = useCopyToClipboard();
|
const copyToClipboard = useCopyToClipboard();
|
||||||
|
|
||||||
const freeFormFields = useMemo(() => {
|
const freeFormFields = useMemo(() => {
|
||||||
|
@ -47,7 +48,7 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||||
})}
|
})}
|
||||||
onClick={createHandlerClick(legend)}
|
onClick={createHandlerClick(legend)}
|
||||||
>
|
>
|
||||||
{!isHeatmap && (
|
{!isAnomalyView && !isHeatmap && (
|
||||||
<div
|
<div
|
||||||
className="vm-legend-item__marker"
|
className="vm-legend-item__marker"
|
||||||
style={{ backgroundColor: legend.color }}
|
style={{ backgroundColor: legend.color }}
|
||||||
|
|
|
@ -9,8 +9,8 @@ type Props = {
|
||||||
|
|
||||||
const titles: Partial<Record<ForecastType, string>> = {
|
const titles: Partial<Record<ForecastType, string>> = {
|
||||||
[ForecastType.yhat]: "yhat",
|
[ForecastType.yhat]: "yhat",
|
||||||
[ForecastType.yhatLower]: "yhat_lower/_upper",
|
[ForecastType.yhatLower]: "yhat_upper - yhat_lower",
|
||||||
[ForecastType.yhatUpper]: "yhat_lower/_upper",
|
[ForecastType.yhatUpper]: "yhat_upper - yhat_lower",
|
||||||
[ForecastType.anomaly]: "anomalies",
|
[ForecastType.anomaly]: "anomalies",
|
||||||
[ForecastType.training]: "training data",
|
[ForecastType.training]: "training data",
|
||||||
[ForecastType.actual]: "y"
|
[ForecastType.actual]: "y"
|
||||||
|
@ -42,9 +42,6 @@ const LegendAnomaly: FC<Props> = ({ series }) => {
|
||||||
}));
|
}));
|
||||||
}, [series]);
|
}, [series]);
|
||||||
|
|
||||||
const container = document.getElementById("legendAnomaly");
|
|
||||||
if (!container) return null;
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className="vm-legend-anomaly">
|
<div className="vm-legend-anomaly">
|
||||||
{/* TODO: remove .filter() after the correct training data has been added */}
|
{/* TODO: remove .filter() after the correct training data has been added */}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export interface LineChartProps {
|
||||||
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
|
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
|
||||||
layoutSize: ElementSize;
|
layoutSize: ElementSize;
|
||||||
height?: number;
|
height?: number;
|
||||||
anomalyView?: boolean;
|
isAnomalyView?: boolean;
|
||||||
spanGaps?: boolean;
|
spanGaps?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
setPeriod,
|
setPeriod,
|
||||||
layoutSize,
|
layoutSize,
|
||||||
height,
|
height,
|
||||||
anomalyView,
|
isAnomalyView,
|
||||||
spanGaps = false
|
spanGaps = false
|
||||||
}) => {
|
}) => {
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
@ -73,7 +73,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
seriesFocus,
|
seriesFocus,
|
||||||
setCursor,
|
setCursor,
|
||||||
resetTooltips
|
resetTooltips
|
||||||
} = useLineTooltip({ u: uPlotInst, metrics, series, unit, anomalyView });
|
} = useLineTooltip({ u: uPlotInst, metrics, series, unit, isAnomalyView });
|
||||||
|
|
||||||
const options: uPlotOptions = {
|
const options: uPlotOptions = {
|
||||||
...getDefaultOptions({ width: layoutSize.width, height }),
|
...getDefaultOptions({ width: layoutSize.width, height }),
|
||||||
|
|
|
@ -12,8 +12,11 @@ import useBoolean from "../../../hooks/useBoolean";
|
||||||
import useEventListener from "../../../hooks/useEventListener";
|
import useEventListener from "../../../hooks/useEventListener";
|
||||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||||
import { AUTOCOMPLETE_QUICK_KEY } from "../../Main/ShortcutKeys/constants/keyList";
|
import { AUTOCOMPLETE_QUICK_KEY } from "../../Main/ShortcutKeys/constants/keyList";
|
||||||
|
import { QueryConfiguratorProps } from "../../../pages/CustomPanel/QueryConfigurator/QueryConfigurator";
|
||||||
|
|
||||||
const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
|
type Props = Pick<QueryConfiguratorProps, "hideButtons">;
|
||||||
|
|
||||||
|
const AdditionalSettingsControls: FC<Props & {isMobile?: boolean}> = ({ isMobile, hideButtons }) => {
|
||||||
const { autocomplete } = useQueryState();
|
const { autocomplete } = useQueryState();
|
||||||
const queryDispatch = useQueryDispatch();
|
const queryDispatch = useQueryDispatch();
|
||||||
|
|
||||||
|
@ -54,31 +57,35 @@ const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
|
||||||
"vm-additional-settings_mobile": isMobile
|
"vm-additional-settings_mobile": isMobile
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Tooltip title={<>Quick tip: {AUTOCOMPLETE_QUICK_KEY}</>}>
|
{!hideButtons?.autocomplete && (
|
||||||
<Switch
|
<Tooltip title={<>Quick tip: {AUTOCOMPLETE_QUICK_KEY}</>}>
|
||||||
label={"Autocomplete"}
|
<Switch
|
||||||
value={autocomplete}
|
label={"Autocomplete"}
|
||||||
onChange={onChangeAutocomplete}
|
value={autocomplete}
|
||||||
fullWidth={isMobile}
|
onChange={onChangeAutocomplete}
|
||||||
/>
|
fullWidth={isMobile}
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Switch
|
<Switch
|
||||||
label={"Disable cache"}
|
label={"Disable cache"}
|
||||||
value={nocache}
|
value={nocache}
|
||||||
onChange={onChangeCache}
|
onChange={onChangeCache}
|
||||||
fullWidth={isMobile}
|
fullWidth={isMobile}
|
||||||
/>
|
/>
|
||||||
<Switch
|
{!hideButtons?.traceQuery && (
|
||||||
label={"Trace query"}
|
<Switch
|
||||||
value={isTracingEnabled}
|
label={"Trace query"}
|
||||||
onChange={onChangeQueryTracing}
|
value={isTracingEnabled}
|
||||||
fullWidth={isMobile}
|
onChange={onChangeQueryTracing}
|
||||||
/>
|
fullWidth={isMobile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AdditionalSettings: FC = () => {
|
const AdditionalSettings: FC<Props> = (props) => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
const targetRef = useRef<HTMLDivElement>(null);
|
const targetRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
@ -106,13 +113,16 @@ const AdditionalSettings: FC = () => {
|
||||||
onClose={handleCloseList}
|
onClose={handleCloseList}
|
||||||
title={"Query settings"}
|
title={"Query settings"}
|
||||||
>
|
>
|
||||||
<AdditionalSettingsControls isMobile={isMobile}/>
|
<AdditionalSettingsControls
|
||||||
|
isMobile={isMobile}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
</Popper>
|
</Popper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AdditionalSettingsControls/>;
|
return <AdditionalSettingsControls {...props}/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdditionalSettings;
|
export default AdditionalSettings;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
import useElementSize from "../../../hooks/useElementSize";
|
import useElementSize from "../../../hooks/useElementSize";
|
||||||
import { ChartTooltipProps } from "../../Chart/ChartTooltip/ChartTooltip";
|
import { ChartTooltipProps } from "../../Chart/ChartTooltip/ChartTooltip";
|
||||||
import LegendAnomaly from "../../Chart/Line/LegendAnomaly/LegendAnomaly";
|
import LegendAnomaly from "../../Chart/Line/LegendAnomaly/LegendAnomaly";
|
||||||
|
import { groupByMultipleKeys } from "../../../utils/array";
|
||||||
|
|
||||||
export interface GraphViewProps {
|
export interface GraphViewProps {
|
||||||
data?: MetricResult[];
|
data?: MetricResult[];
|
||||||
|
@ -40,7 +41,7 @@ export interface GraphViewProps {
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
height?: number;
|
height?: number;
|
||||||
isHistogram?: boolean;
|
isHistogram?: boolean;
|
||||||
anomalyView?: boolean;
|
isAnomalyView?: boolean;
|
||||||
spanGaps?: boolean;
|
spanGaps?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
fullWidth = true,
|
fullWidth = true,
|
||||||
height,
|
height,
|
||||||
isHistogram,
|
isHistogram,
|
||||||
anomalyView,
|
isAnomalyView,
|
||||||
spanGaps
|
spanGaps
|
||||||
}) => {
|
}) => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
@ -74,8 +75,8 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
const [legendValue, setLegendValue] = useState<ChartTooltipProps | null>(null);
|
const [legendValue, setLegendValue] = useState<ChartTooltipProps | null>(null);
|
||||||
|
|
||||||
const getSeriesItem = useMemo(() => {
|
const getSeriesItem = useMemo(() => {
|
||||||
return getSeriesItemContext(data, hideSeries, alias, anomalyView);
|
return getSeriesItemContext(data, hideSeries, alias, isAnomalyView);
|
||||||
}, [data, hideSeries, alias, anomalyView]);
|
}, [data, hideSeries, alias, isAnomalyView]);
|
||||||
|
|
||||||
const setLimitsYaxis = (values: { [key: string]: number[] }) => {
|
const setLimitsYaxis = (values: { [key: string]: number[] }) => {
|
||||||
const limits = getLimitsYAxis(values, !isHistogram);
|
const limits = getLimitsYAxis(values, !isHistogram);
|
||||||
|
@ -83,7 +84,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeLegend = (legend: LegendItemType, metaKey: boolean) => {
|
const onChangeLegend = (legend: LegendItemType, metaKey: boolean) => {
|
||||||
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
|
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series, isAnomalyView }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const prepareHistogramData = (data: (number | null)[][]) => {
|
const prepareHistogramData = (data: (number | null)[][]) => {
|
||||||
|
@ -108,6 +109,20 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
return [null, [xs, ys, counts]];
|
return [null, [xs, ys, counts]];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const prepareAnomalyLegend = (legend: LegendItemType[]): LegendItemType[] => {
|
||||||
|
if (!isAnomalyView) return legend;
|
||||||
|
|
||||||
|
// For vmanomaly: Only select the first series per group (due to API specs) and clear __name__ in freeFormFields.
|
||||||
|
const grouped = groupByMultipleKeys(legend, ["group", "label"]);
|
||||||
|
return grouped.map((group) => {
|
||||||
|
const firstEl = group.values[0];
|
||||||
|
return {
|
||||||
|
...firstEl,
|
||||||
|
freeFormFields: { ...firstEl.freeFormFields, __name__: "" }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tempTimes: number[] = [];
|
const tempTimes: number[] = [];
|
||||||
const tempValues: { [key: string]: number[] } = {};
|
const tempValues: { [key: string]: number[] } = {};
|
||||||
|
@ -153,14 +168,18 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
const range = getMinMaxBuffer(getMinFromArray(resultAsNumber), getMaxFromArray(resultAsNumber));
|
const range = getMinMaxBuffer(getMinFromArray(resultAsNumber), getMaxFromArray(resultAsNumber));
|
||||||
const rangeStep = Math.abs(range[1] - range[0]);
|
const rangeStep = Math.abs(range[1] - range[0]);
|
||||||
|
|
||||||
return (avg > rangeStep * 1e10) && !anomalyView ? results.map(() => avg) : results;
|
return (avg > rangeStep * 1e10) && !isAnomalyView ? results.map(() => avg) : results;
|
||||||
});
|
});
|
||||||
timeDataSeries.unshift(timeSeries);
|
timeDataSeries.unshift(timeSeries);
|
||||||
setLimitsYaxis(tempValues);
|
setLimitsYaxis(tempValues);
|
||||||
const result = isHistogram ? prepareHistogramData(timeDataSeries) : timeDataSeries;
|
const result = isHistogram ? prepareHistogramData(timeDataSeries) : timeDataSeries;
|
||||||
setDataChart(result as uPlotData);
|
setDataChart(result as uPlotData);
|
||||||
setSeries(tempSeries);
|
setSeries(tempSeries);
|
||||||
setLegend(tempLegend);
|
const legend = prepareAnomalyLegend(tempLegend);
|
||||||
|
setLegend(legend);
|
||||||
|
if (isAnomalyView) {
|
||||||
|
setHideSeries(legend.map(s => s.label || "").slice(1));
|
||||||
|
}
|
||||||
}, [data, timezone, isHistogram]);
|
}, [data, timezone, isHistogram]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -172,7 +191,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
tempLegend.push(getLegendItem(seriesItem, d.group));
|
tempLegend.push(getLegendItem(seriesItem, d.group));
|
||||||
});
|
});
|
||||||
setSeries(tempSeries);
|
setSeries(tempSeries);
|
||||||
setLegend(tempLegend);
|
setLegend(prepareAnomalyLegend(tempLegend));
|
||||||
}, [hideSeries]);
|
}, [hideSeries]);
|
||||||
|
|
||||||
const [containerRef, containerSize] = useElementSize();
|
const [containerRef, containerSize] = useElementSize();
|
||||||
|
@ -197,7 +216,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
setPeriod={setPeriod}
|
setPeriod={setPeriod}
|
||||||
layoutSize={containerSize}
|
layoutSize={containerSize}
|
||||||
height={height}
|
height={height}
|
||||||
anomalyView={anomalyView}
|
isAnomalyView={isAnomalyView}
|
||||||
spanGaps={spanGaps}
|
spanGaps={spanGaps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -213,10 +232,12 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
onChangeLegend={setLegendValue}
|
onChangeLegend={setLegendValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isHistogram && !anomalyView && showLegend && (
|
{isAnomalyView && showLegend && (<LegendAnomaly series={series as SeriesItem[]}/>)}
|
||||||
|
{!isHistogram && showLegend && (
|
||||||
<Legend
|
<Legend
|
||||||
labels={legend}
|
labels={legend}
|
||||||
query={query}
|
query={query}
|
||||||
|
isAnomalyView={isAnomalyView}
|
||||||
onChange={onChangeLegend}
|
onChange={onChangeLegend}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -228,11 +249,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
legendValue={legendValue}
|
legendValue={legendValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{anomalyView && showLegend && (
|
|
||||||
<LegendAnomaly
|
|
||||||
series={series as SeriesItem[]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -62,10 +62,6 @@ export const anomalyNavigation: NavigationItem[] = [
|
||||||
{
|
{
|
||||||
label: routerOptions[router.anomaly].title,
|
label: routerOptions[router.anomaly].title,
|
||||||
value: router.home,
|
value: router.home,
|
||||||
},
|
|
||||||
{
|
|
||||||
label: routerOptions[router.home].title,
|
|
||||||
value: router.query,
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,10 @@ interface LineTooltipHook {
|
||||||
metrics: MetricResult[];
|
metrics: MetricResult[];
|
||||||
series: uPlotSeries[];
|
series: uPlotSeries[];
|
||||||
unit?: string;
|
unit?: string;
|
||||||
anomalyView?: boolean;
|
isAnomalyView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useLineTooltip = ({ u, metrics, series, unit, anomalyView }: LineTooltipHook) => {
|
const useLineTooltip = ({ u, metrics, series, unit, isAnomalyView }: LineTooltipHook) => {
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
||||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
||||||
|
@ -61,14 +61,14 @@ const useLineTooltip = ({ u, metrics, series, unit, anomalyView }: LineTooltipHo
|
||||||
point,
|
point,
|
||||||
u: u,
|
u: u,
|
||||||
id: `${seriesIdx}_${dataIdx}`,
|
id: `${seriesIdx}_${dataIdx}`,
|
||||||
title: groups.size > 1 && !anomalyView ? `Query ${group}` : "",
|
title: groups.size > 1 && !isAnomalyView ? `Query ${group}` : "",
|
||||||
dates: [date ? dayjs(date * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT) : "-"],
|
dates: [date ? dayjs(date * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT) : "-"],
|
||||||
value: formatPrettyNumber(value, min, max),
|
value: formatPrettyNumber(value, min, max),
|
||||||
info: getMetricName(metricItem),
|
info: getMetricName(metricItem),
|
||||||
statsFormatted: seriesItem?.statsFormatted,
|
statsFormatted: seriesItem?.statsFormatted,
|
||||||
marker: `${seriesItem?.stroke}`,
|
marker: `${seriesItem?.stroke}`,
|
||||||
};
|
};
|
||||||
}, [u, tooltipIdx, metrics, series, unit, anomalyView]);
|
}, [u, tooltipIdx, metrics, series, unit, isAnomalyView]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (!showTooltip) return;
|
if (!showTooltip) return;
|
||||||
|
|
|
@ -12,7 +12,8 @@ import { useTimeState } from "../state/time/TimeStateContext";
|
||||||
import { useCustomPanelState } from "../state/customPanel/CustomPanelStateContext";
|
import { useCustomPanelState } from "../state/customPanel/CustomPanelStateContext";
|
||||||
import { isHistogramData } from "../utils/metric";
|
import { isHistogramData } from "../utils/metric";
|
||||||
import { useGraphState } from "../state/graph/GraphStateContext";
|
import { useGraphState } from "../state/graph/GraphStateContext";
|
||||||
import { getStepFromDuration } from "../utils/time";
|
import { getSecondsFromDuration, getStepFromDuration } from "../utils/time";
|
||||||
|
import { AppType } from "../types/appType";
|
||||||
|
|
||||||
interface FetchQueryParams {
|
interface FetchQueryParams {
|
||||||
predefinedQuery?: string[]
|
predefinedQuery?: string[]
|
||||||
|
@ -47,13 +48,15 @@ interface FetchDataParams {
|
||||||
hideQuery?: number[]
|
hideQuery?: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAnomalyUI = AppType.anomaly === process.env.REACT_APP_TYPE;
|
||||||
|
|
||||||
export const useFetchQuery = ({
|
export const useFetchQuery = ({
|
||||||
predefinedQuery,
|
predefinedQuery,
|
||||||
visible,
|
visible,
|
||||||
display,
|
display,
|
||||||
customStep,
|
customStep,
|
||||||
hideQuery,
|
hideQuery,
|
||||||
showAllSeries
|
showAllSeries,
|
||||||
}: FetchQueryParams): FetchQueryReturn => {
|
}: FetchQueryParams): FetchQueryReturn => {
|
||||||
const { query } = useQueryState();
|
const { query } = useQueryState();
|
||||||
const { period } = useTimeState();
|
const { period } = useTimeState();
|
||||||
|
@ -124,7 +127,7 @@ export const useFetchQuery = ({
|
||||||
tempTraces.push(trace);
|
tempTraces.push(trace);
|
||||||
}
|
}
|
||||||
|
|
||||||
isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
|
isHistogramResult = !isAnomalyUI && isDisplayChart && isHistogramData(resp.data.result);
|
||||||
seriesLimit = isHistogramResult ? Infinity : defaultLimit;
|
seriesLimit = isHistogramResult ? Infinity : defaultLimit;
|
||||||
const freeTempSize = seriesLimit - tempData.length;
|
const freeTempSize = seriesLimit - tempData.length;
|
||||||
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
|
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
|
||||||
|
@ -172,7 +175,7 @@ export const useFetchQuery = ({
|
||||||
setQueryErrors(expr.map(() => ErrorTypes.validQuery));
|
setQueryErrors(expr.map(() => ErrorTypes.validQuery));
|
||||||
} else if (isValidHttpUrl(serverUrl)) {
|
} else if (isValidHttpUrl(serverUrl)) {
|
||||||
const updatedPeriod = { ...period };
|
const updatedPeriod = { ...period };
|
||||||
updatedPeriod.step = customStep;
|
updatedPeriod.step = isAnomalyUI ? `${getSecondsFromDuration(customStep)*1000}ms` : customStep;
|
||||||
return expr.map(q => displayChart
|
return expr.map(q => displayChart
|
||||||
? getQueryRangeUrl(serverUrl, q, updatedPeriod, nocache, isTracingEnabled)
|
? getQueryRangeUrl(serverUrl, q, updatedPeriod, nocache, isTracingEnabled)
|
||||||
: getQueryUrl(serverUrl, q, updatedPeriod, nocache, isTracingEnabled));
|
: getQueryUrl(serverUrl, q, updatedPeriod, nocache, isTracingEnabled));
|
||||||
|
|
|
@ -14,10 +14,10 @@ type Props = {
|
||||||
isHistogram: boolean;
|
isHistogram: boolean;
|
||||||
graphData: MetricResult[];
|
graphData: MetricResult[];
|
||||||
controlsRef: React.RefObject<HTMLDivElement>;
|
controlsRef: React.RefObject<HTMLDivElement>;
|
||||||
anomalyView?: boolean;
|
isAnomalyView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphTab: FC<Props> = ({ isHistogram, graphData, controlsRef, anomalyView }) => {
|
const GraphTab: FC<Props> = ({ isHistogram, graphData, controlsRef, isAnomalyView }) => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const { customStep, yaxis, spanGaps } = useGraphState();
|
const { customStep, yaxis, spanGaps } = useGraphState();
|
||||||
|
@ -68,7 +68,7 @@ const GraphTab: FC<Props> = ({ isHistogram, graphData, controlsRef, anomalyView
|
||||||
setPeriod={setPeriod}
|
setPeriod={setPeriod}
|
||||||
height={isMobile ? window.innerHeight * 0.5 : 500}
|
height={isMobile ? window.innerHeight * 0.5 : 500}
|
||||||
isHistogram={isHistogram}
|
isHistogram={isHistogram}
|
||||||
anomalyView={anomalyView}
|
isAnomalyView={isAnomalyView}
|
||||||
spanGaps={spanGaps}
|
spanGaps={spanGaps}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -30,8 +30,14 @@ export interface QueryConfiguratorProps {
|
||||||
setQueryErrors: StateUpdater<string[]>;
|
setQueryErrors: StateUpdater<string[]>;
|
||||||
setHideError: StateUpdater<boolean>;
|
setHideError: StateUpdater<boolean>;
|
||||||
stats: QueryStats[];
|
stats: QueryStats[];
|
||||||
onHideQuery: (queries: number[]) => void
|
onHideQuery?: (queries: number[]) => void
|
||||||
onRunQuery: () => void
|
onRunQuery: () => void;
|
||||||
|
hideButtons?: {
|
||||||
|
addQuery?: boolean;
|
||||||
|
prettify?: boolean;
|
||||||
|
autocomplete?: boolean;
|
||||||
|
traceQuery?: boolean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||||
|
@ -40,7 +46,8 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||||
setHideError,
|
setHideError,
|
||||||
stats,
|
stats,
|
||||||
onHideQuery,
|
onHideQuery,
|
||||||
onRunQuery
|
onRunQuery,
|
||||||
|
hideButtons
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
@ -159,7 +166,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||||
}, [stateQuery]);
|
}, [stateQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onHideQuery(hideQuery);
|
onHideQuery && onHideQuery(hideQuery);
|
||||||
}, [hideQuery]);
|
}, [hideQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -188,40 +195,43 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||||
>
|
>
|
||||||
<QueryEditor
|
<QueryEditor
|
||||||
value={stateQuery[i]}
|
value={stateQuery[i]}
|
||||||
autocomplete={autocomplete || autocompleteQuick}
|
autocomplete={!hideButtons?.autocomplete && (autocomplete || autocompleteQuick)}
|
||||||
error={queryErrors[i]}
|
error={queryErrors[i]}
|
||||||
stats={stats[i]}
|
stats={stats[i]}
|
||||||
onArrowUp={createHandlerArrow(-1, i)}
|
onArrowUp={createHandlerArrow(-1, i)}
|
||||||
onArrowDown={createHandlerArrow(1, i)}
|
onArrowDown={createHandlerArrow(1, i)}
|
||||||
onEnter={handleRunQuery}
|
onEnter={handleRunQuery}
|
||||||
onChange={createHandlerChangeQuery(i)}
|
onChange={createHandlerChangeQuery(i)}
|
||||||
label={`Query ${i + 1}`}
|
label={`Query ${stateQuery.length > 1 ? i + 1 : ""}`}
|
||||||
disabled={hideQuery.includes(i)}
|
disabled={hideQuery.includes(i)}
|
||||||
/>
|
/>
|
||||||
<Tooltip title={hideQuery.includes(i) ? "Enable query" : "Disable query"}>
|
{onHideQuery && (
|
||||||
<div className="vm-query-configurator-list-row__button">
|
<Tooltip title={hideQuery.includes(i) ? "Enable query" : "Disable query"}>
|
||||||
<Button
|
<div className="vm-query-configurator-list-row__button">
|
||||||
variant={"text"}
|
<Button
|
||||||
color={"gray"}
|
variant={"text"}
|
||||||
startIcon={hideQuery.includes(i) ? <VisibilityOffIcon/> : <VisibilityIcon/>}
|
color={"gray"}
|
||||||
onClick={createHandlerHideQuery(i)}
|
startIcon={hideQuery.includes(i) ? <VisibilityOffIcon/> : <VisibilityIcon/>}
|
||||||
ariaLabel="visibility query"
|
onClick={createHandlerHideQuery(i)}
|
||||||
/>
|
ariaLabel="visibility query"
|
||||||
</div>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip title={"Prettify query"}>
|
{!hideButtons?.prettify && (
|
||||||
<div className="vm-query-configurator-list-row__button">
|
<Tooltip title={"Prettify query"}>
|
||||||
<Button
|
<div className="vm-query-configurator-list-row__button">
|
||||||
variant={"text"}
|
<Button
|
||||||
color={"gray"}
|
variant={"text"}
|
||||||
startIcon={<Prettify/>}
|
color={"gray"}
|
||||||
onClick={async () => await handlePrettifyQuery(i)}
|
startIcon={<Prettify/>}
|
||||||
className="prettify"
|
onClick={async () => await handlePrettifyQuery(i)}
|
||||||
ariaLabel="prettify the query"
|
className="prettify"
|
||||||
/>
|
ariaLabel="prettify the query"
|
||||||
</div>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
|
</Tooltip>)}
|
||||||
|
|
||||||
{stateQuery.length > 1 && (
|
{stateQuery.length > 1 && (
|
||||||
<Tooltip title="Remove Query">
|
<Tooltip title="Remove Query">
|
||||||
|
@ -240,10 +250,10 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-query-configurator-settings">
|
<div className="vm-query-configurator-settings">
|
||||||
<AdditionalSettings/>
|
<AdditionalSettings hideButtons={hideButtons}/>
|
||||||
<div className="vm-query-configurator-settings__buttons">
|
<div className="vm-query-configurator-settings__buttons">
|
||||||
<QueryHistory handleSelectQuery={handleSelectHistory}/>
|
<QueryHistory handleSelectQuery={handleSelectHistory}/>
|
||||||
{stateQuery.length < MAX_QUERY_FIELDS && (
|
{!hideButtons?.addQuery && stateQuery.length < MAX_QUERY_FIELDS && (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleAddQuery}
|
onClick={handleAddQuery}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: space-between;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
margin: -$padding-medium 0-$padding-medium $padding-medium;
|
margin: -$padding-medium 0-$padding-medium $padding-medium;
|
||||||
padding: 0 $padding-medium;
|
padding: 0 $padding-medium;
|
||||||
|
|
|
@ -1,60 +1,63 @@
|
||||||
import React, { FC, useMemo, useRef } from "preact/compat";
|
import React, { FC, useMemo, useRef, useState } from "preact/compat";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||||
import useEventListener from "../../hooks/useEventListener";
|
import { ForecastType } from "../../types";
|
||||||
|
import { useSetQueryParams } from "../CustomPanel/hooks/useSetQueryParams";
|
||||||
|
import QueryConfigurator from "../CustomPanel/QueryConfigurator/QueryConfigurator";
|
||||||
import "../CustomPanel/style.scss";
|
import "../CustomPanel/style.scss";
|
||||||
import ExploreAnomalyHeader from "./ExploreAnomalyHeader/ExploreAnomalyHeader";
|
import { useQueryState } from "../../state/query/QueryStateContext";
|
||||||
import Alert from "../../components/Main/Alert/Alert";
|
|
||||||
import { extractFields, isForecast } from "../../utils/uplot";
|
|
||||||
import { useFetchQuery } from "../../hooks/useFetchQuery";
|
import { useFetchQuery } from "../../hooks/useFetchQuery";
|
||||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
|
||||||
import GraphTab from "../CustomPanel/CustomPanelTabs/GraphTab";
|
|
||||||
import { useGraphState } from "../../state/graph/GraphStateContext";
|
import { useGraphState } from "../../state/graph/GraphStateContext";
|
||||||
|
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||||
|
import Alert from "../../components/Main/Alert/Alert";
|
||||||
|
import WarningLimitSeries from "../CustomPanel/WarningLimitSeries/WarningLimitSeries";
|
||||||
|
import GraphTab from "../CustomPanel/CustomPanelTabs/GraphTab";
|
||||||
|
import { extractFields, isForecast } from "../../utils/uplot";
|
||||||
import { MetricResult } from "../../api/types";
|
import { MetricResult } from "../../api/types";
|
||||||
import { promValueToNumber } from "../../utils/metric";
|
import { promValueToNumber } from "../../utils/metric";
|
||||||
import { ForecastType } from "../../types";
|
|
||||||
import { useFetchAnomalySeries } from "./hooks/useFetchAnomalySeries";
|
|
||||||
import { useQueryDispatch } from "../../state/query/QueryStateContext";
|
|
||||||
import { useTimeDispatch } from "../../state/time/TimeStateContext";
|
|
||||||
|
|
||||||
const anomalySeries = [
|
|
||||||
ForecastType.yhat,
|
|
||||||
ForecastType.yhatUpper,
|
|
||||||
ForecastType.yhatLower,
|
|
||||||
ForecastType.anomalyScore
|
|
||||||
];
|
|
||||||
|
|
||||||
// Hardcoded to 1.0 for now; consider adding a UI slider for threshold adjustment in the future.
|
// Hardcoded to 1.0 for now; consider adding a UI slider for threshold adjustment in the future.
|
||||||
const ANOMALY_SCORE_THRESHOLD = 1;
|
const ANOMALY_SCORE_THRESHOLD = 1;
|
||||||
|
|
||||||
const ExploreAnomaly: FC = () => {
|
const ExploreAnomaly: FC = () => {
|
||||||
|
useSetQueryParams();
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const queryDispatch = useQueryDispatch();
|
const { query } = useQueryState();
|
||||||
const timeDispatch = useTimeDispatch();
|
|
||||||
const { series, error: errorSeries, isLoading: isAnomalySeriesLoading } = useFetchAnomalySeries();
|
|
||||||
const queries = useMemo(() => series ? Object.keys(series) : [], [series]);
|
|
||||||
|
|
||||||
const controlsRef = useRef<HTMLDivElement>(null);
|
|
||||||
const { customStep } = useGraphState();
|
const { customStep } = useGraphState();
|
||||||
|
|
||||||
const { graphData, error, queryErrors, isHistogram, isLoading: isGraphDataLoading } = useFetchQuery({
|
const controlsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [hideQuery] = useState<number[]>([]);
|
||||||
|
const [hideError, setHideError] = useState(!query[0]);
|
||||||
|
const [showAllSeries, setShowAllSeries] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
graphData,
|
||||||
|
error,
|
||||||
|
queryErrors,
|
||||||
|
setQueryErrors,
|
||||||
|
queryStats,
|
||||||
|
warning,
|
||||||
|
} = useFetchQuery({
|
||||||
visible: true,
|
visible: true,
|
||||||
customStep,
|
customStep,
|
||||||
showAllSeries: true,
|
hideQuery,
|
||||||
|
showAllSeries
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
if (!graphData) return;
|
if (!graphData) return [];
|
||||||
const detectedData = graphData.map(d => ({ ...isForecast(d.metric), ...d }));
|
const detectedData = graphData.map(d => ({ ...isForecast(d.metric), ...d }));
|
||||||
const realData = detectedData.filter(d => d.value === null);
|
const realData = detectedData.filter(d => d.value === ForecastType.actual);
|
||||||
const anomalyScoreData = detectedData.filter(d => d.isAnomalyScore);
|
const anomalyScoreData = detectedData.filter(d => d.value === ForecastType.anomaly);
|
||||||
const anomalyData: MetricResult[] = realData.map((d) => {
|
const anomalyData: MetricResult[] = realData.map((d) => {
|
||||||
const id = extractFields(d.metric);
|
const id = extractFields(d.metric);
|
||||||
const anomalyScoreDataByLabels = anomalyScoreData.find(du => extractFields(du.metric) === id);
|
const anomalyScoreDataByLabels = anomalyScoreData.find(du => extractFields(du.metric) === id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
group: queries.length + 1,
|
group: 1,
|
||||||
metric: { ...d.metric, __name__: ForecastType.anomaly },
|
metric: { ...d.metric, __name__: ForecastType.anomaly },
|
||||||
values: d.values.filter(([t]) => {
|
values: d.values.filter(([t]) => {
|
||||||
if (!anomalyScoreDataByLabels) return false;
|
if (!anomalyScoreDataByLabels) return false;
|
||||||
|
@ -63,23 +66,14 @@ const ExploreAnomaly: FC = () => {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return graphData.filter(d => d.group !== anomalyScoreData[0]?.group).concat(anomalyData);
|
const filterData = detectedData.filter(d => (d.value !== ForecastType.anomaly) && d.value) as MetricResult[];
|
||||||
|
return filterData.concat(anomalyData);
|
||||||
}, [graphData]);
|
}, [graphData]);
|
||||||
|
|
||||||
const onChangeFilter = (expr: Record<string, string>) => {
|
const handleRunQuery = () => {
|
||||||
const { __name__ = "", ...labelValue } = expr;
|
setHideError(false);
|
||||||
let prefix = __name__.replace(/y|_y/, "");
|
|
||||||
if (prefix) prefix += "_";
|
|
||||||
const metrics = [__name__, ...anomalySeries];
|
|
||||||
const filters = Object.entries(labelValue).map(([key, value]) => `${key}="${value}"`).join(",");
|
|
||||||
const queries = metrics.map((m, i) => `${i ? prefix : ""}${m}{${filters}}`);
|
|
||||||
queryDispatch({ type: "SET_QUERY", payload: queries });
|
|
||||||
timeDispatch({ type: "RUN_QUERY" });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangePopstate = () => window.location.reload();
|
|
||||||
useEventListener("popstate", handleChangePopstate);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
@ -87,14 +81,23 @@ const ExploreAnomaly: FC = () => {
|
||||||
"vm-custom-panel_mobile": isMobile,
|
"vm-custom-panel_mobile": isMobile,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ExploreAnomalyHeader
|
<QueryConfigurator
|
||||||
queries={queries}
|
queryErrors={!hideError ? queryErrors : []}
|
||||||
series={series}
|
setQueryErrors={setQueryErrors}
|
||||||
onChange={onChangeFilter}
|
setHideError={setHideError}
|
||||||
|
stats={queryStats}
|
||||||
|
onRunQuery={handleRunQuery}
|
||||||
|
hideButtons={{ addQuery: true, prettify: true, autocomplete: true, traceQuery: true }}
|
||||||
/>
|
/>
|
||||||
{(isGraphDataLoading || isAnomalySeriesLoading) && <Spinner />}
|
{isLoading && <Spinner/>}
|
||||||
{(error || errorSeries) && <Alert variant="error">{error || errorSeries}</Alert>}
|
{(!hideError && error) && <Alert variant="error">{error}</Alert>}
|
||||||
{!error && !errorSeries && queryErrors?.[0] && <Alert variant="error">{queryErrors[0]}</Alert>}
|
{warning && (
|
||||||
|
<WarningLimitSeries
|
||||||
|
warning={warning}
|
||||||
|
query={query}
|
||||||
|
onChange={setShowAllSeries}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-custom-panel-body": true,
|
"vm-custom-panel-body": true,
|
||||||
|
@ -112,9 +115,9 @@ const ExploreAnomaly: FC = () => {
|
||||||
{data && (
|
{data && (
|
||||||
<GraphTab
|
<GraphTab
|
||||||
graphData={data}
|
graphData={data}
|
||||||
isHistogram={isHistogram}
|
isHistogram={false}
|
||||||
controlsRef={controlsRef}
|
controlsRef={controlsRef}
|
||||||
anomalyView={true}
|
isAnomalyView={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
import React, { FC, useMemo, useState } from "preact/compat";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
|
||||||
import Select from "../../../components/Main/Select/Select";
|
|
||||||
import "./style.scss";
|
|
||||||
import usePrevious from "../../../hooks/usePrevious";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { arrayEquals } from "../../../utils/array";
|
|
||||||
import { getQueryStringValue } from "../../../utils/query-string";
|
|
||||||
import { useSetQueryParams } from "../hooks/useSetQueryParams";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
queries: string[];
|
|
||||||
series?: Record<string, {[p: string]: string}[]>
|
|
||||||
onChange: (expr: Record<string, string>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExploreAnomalyHeader: FC<Props> = ({ queries, series, onChange }) => {
|
|
||||||
const { isMobile } = useDeviceDetect();
|
|
||||||
const [alias, setAlias] = useState(queries[0]);
|
|
||||||
const [selectedValues, setSelectedValues] = useState<Record<string, string>>({});
|
|
||||||
useSetQueryParams({ alias: alias, ...selectedValues });
|
|
||||||
|
|
||||||
const uniqueKeysWithValues = useMemo(() => {
|
|
||||||
if (!series) return {};
|
|
||||||
return series[alias]?.reduce((accumulator, currentSeries) => {
|
|
||||||
const metric = Object.entries(currentSeries);
|
|
||||||
if (!metric.length) return accumulator;
|
|
||||||
const excludeMetrics = ["__name__", "for"];
|
|
||||||
for (const [key, value] of metric) {
|
|
||||||
if (excludeMetrics.includes(key) || accumulator[key]?.includes(value)) continue;
|
|
||||||
|
|
||||||
if (!accumulator[key]) {
|
|
||||||
accumulator[key] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
accumulator[key].push(value);
|
|
||||||
}
|
|
||||||
return accumulator;
|
|
||||||
}, {} as Record<string, string[]>) || {};
|
|
||||||
}, [alias, series]);
|
|
||||||
const prevUniqueKeysWithValues = usePrevious(uniqueKeysWithValues);
|
|
||||||
|
|
||||||
const createHandlerChangeSelect = (key: string) => (value: string) => {
|
|
||||||
setSelectedValues((prev) => ({ ...prev, [key]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const nextValues = Object.values(uniqueKeysWithValues).flat();
|
|
||||||
const prevValues = Object.values(prevUniqueKeysWithValues || {}).flat();
|
|
||||||
if (arrayEquals(prevValues, nextValues)) return;
|
|
||||||
const newSelectedValues: Record<string, string> = {};
|
|
||||||
Object.keys(uniqueKeysWithValues).forEach((key) => {
|
|
||||||
const value = getQueryStringValue(key, "") as string;
|
|
||||||
newSelectedValues[key] = value || uniqueKeysWithValues[key]?.[0];
|
|
||||||
});
|
|
||||||
setSelectedValues(newSelectedValues);
|
|
||||||
}, [uniqueKeysWithValues, prevUniqueKeysWithValues]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!alias || !Object.keys(selectedValues).length) return;
|
|
||||||
const __name__ = series?.[alias]?.[0]?.__name__ || "";
|
|
||||||
onChange({ ...selectedValues, for: alias, __name__ });
|
|
||||||
}, [selectedValues, alias]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAlias(getQueryStringValue("alias", queries[0]) as string);
|
|
||||||
}, [series]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="legendAnomaly"
|
|
||||||
className={classNames({
|
|
||||||
"vm-explore-anomaly-header": true,
|
|
||||||
"vm-explore-anomaly-header_mobile": isMobile,
|
|
||||||
"vm-block": true,
|
|
||||||
"vm-block_mobile": isMobile,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="vm-explore-anomaly-header-main">
|
|
||||||
<div className="vm-explore-anomaly-header__select">
|
|
||||||
<Select
|
|
||||||
value={alias}
|
|
||||||
list={queries}
|
|
||||||
label="Query"
|
|
||||||
placeholder="Please select query"
|
|
||||||
onChange={setAlias}
|
|
||||||
searchable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{Object.entries(uniqueKeysWithValues).map(([key, values]) => (
|
|
||||||
<div
|
|
||||||
className="vm-explore-anomaly-header__values"
|
|
||||||
key={key}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={selectedValues[key] || ""}
|
|
||||||
list={values}
|
|
||||||
label={key}
|
|
||||||
placeholder={`Please select ${key}`}
|
|
||||||
onChange={createHandlerChangeSelect(key)}
|
|
||||||
searchable={values.length > 2}
|
|
||||||
disabled={values.length === 1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExploreAnomalyHeader;
|
|
|
@ -1,37 +0,0 @@
|
||||||
@use "src/styles/variables" as *;
|
|
||||||
|
|
||||||
.vm-explore-anomaly-header {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: $padding-global calc($padding-small + 10px);
|
|
||||||
max-width: calc(100vw - var(--scrollbar-width));
|
|
||||||
|
|
||||||
&_mobile {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-main {
|
|
||||||
display: grid;
|
|
||||||
gap: $padding-large;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&__config {
|
|
||||||
text-transform: lowercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__select {
|
|
||||||
flex-grow: 1;
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__values {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
import { useMemo, useState } from "preact/compat";
|
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
|
||||||
import { ErrorTypes } from "../../../types";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { MetricBase } from "../../../api/types";
|
|
||||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
// TODO: Change the method of retrieving aliases from the configuration after the API has been added
|
|
||||||
const seriesQuery = `{
|
|
||||||
for!="",
|
|
||||||
__name__!~".*yhat.*|.*trend.*|.*anomaly_score.*|.*daily.*|.*additive_terms.*|.*multiplicative_terms.*|.*weekly.*"
|
|
||||||
}`;
|
|
||||||
|
|
||||||
export const useFetchAnomalySeries = () => {
|
|
||||||
const { serverUrl } = useAppState();
|
|
||||||
const { period: { start, end } } = useTimeState();
|
|
||||||
|
|
||||||
const [series, setSeries] = useState<Record<string, MetricBase["metric"][]>>();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<ErrorTypes | string>();
|
|
||||||
|
|
||||||
// TODO add cached metrics by date
|
|
||||||
const fetchUrl = useMemo(() => {
|
|
||||||
const startDay = dayjs(start * 1000).startOf("day").valueOf() / 1000;
|
|
||||||
const endDay = dayjs(end * 1000).endOf("day").valueOf() / 1000;
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
"match[]": seriesQuery,
|
|
||||||
start: `${startDay}`,
|
|
||||||
end: `${endDay}`
|
|
||||||
});
|
|
||||||
|
|
||||||
return `${serverUrl}/api/v1/series?${params}`;
|
|
||||||
}, [serverUrl, start, end]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSeries = async () => {
|
|
||||||
setError("");
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await fetch(fetchUrl);
|
|
||||||
const resp = await response.json();
|
|
||||||
const data = (resp?.data || []) as MetricBase["metric"][];
|
|
||||||
const groupedByFor = data.reduce<{ [key: string]: MetricBase["metric"][] }>((acc, item) => {
|
|
||||||
const forKey = item["for"];
|
|
||||||
if (!acc[forKey]) acc[forKey] = [];
|
|
||||||
acc[forKey].push(item);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
setSeries(groupedByFor);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorType = resp.errorType ? `${resp.errorType}\r\n` : "";
|
|
||||||
setError(`${errorType}${resp?.error || resp?.message}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error && e.name !== "AbortError") {
|
|
||||||
const message = e.name === "SyntaxError" ? ErrorTypes.unknownType : `${e.name}: ${e.message}`;
|
|
||||||
setError(`${message}`);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchSeries();
|
|
||||||
}, [fetchUrl]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
series,
|
|
||||||
isLoading,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { useEffect } from "react";
|
|
||||||
import { compactObject } from "../../../utils/object";
|
|
||||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
|
||||||
import { useGraphState } from "../../../state/graph/GraphStateContext";
|
|
||||||
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
|
|
||||||
|
|
||||||
interface stateParams extends Record<string, string> {
|
|
||||||
alias: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSetQueryParams = ({ alias, ...args }: stateParams) => {
|
|
||||||
const { duration, relativeTime, period: { date } } = useTimeState();
|
|
||||||
const { customStep } = useGraphState();
|
|
||||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
|
||||||
|
|
||||||
const setSearchParamsFromState = () => {
|
|
||||||
const params = compactObject({
|
|
||||||
["g0.range_input"]: duration,
|
|
||||||
["g0.end_input"]: date,
|
|
||||||
["g0.step_input"]: customStep,
|
|
||||||
["g0.relative_time"]: relativeTime,
|
|
||||||
alias,
|
|
||||||
...args,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSearchParamsFromKeys(params);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(setSearchParamsFromState, [duration, relativeTime, date, customStep, alias, args]);
|
|
||||||
useEffect(setSearchParamsFromState, []);
|
|
||||||
};
|
|
|
@ -29,7 +29,8 @@ export interface HideSeriesArgs {
|
||||||
hideSeries: string[],
|
hideSeries: string[],
|
||||||
legend: LegendItemType,
|
legend: LegendItemType,
|
||||||
metaKey: boolean,
|
metaKey: boolean,
|
||||||
series: Series[]
|
series: Series[],
|
||||||
|
isAnomalyView?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MinMax = { min: number, max: number }
|
export type MinMax = { min: number, max: number }
|
||||||
|
|
|
@ -8,9 +8,16 @@ export const getDefaultServer = (tenantId?: string): string => {
|
||||||
const { serverURL } = getAppModeParams();
|
const { serverURL } = getAppModeParams();
|
||||||
const storageURL = getFromStorage("SERVER_URL") as string;
|
const storageURL = getFromStorage("SERVER_URL") as string;
|
||||||
const logsURL = window.location.href.replace(/\/(select\/)?(vmui)\/.*/, "");
|
const logsURL = window.location.href.replace(/\/(select\/)?(vmui)\/.*/, "");
|
||||||
|
const anomalyURL = window.location.href.replace(/(?:graph|vmui)\/.*/, "");
|
||||||
const defaultURL = window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
|
const defaultURL = window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
|
||||||
const url = serverURL || storageURL || defaultURL;
|
const url = serverURL || storageURL || defaultURL;
|
||||||
if (REACT_APP_TYPE === AppType.logs) return logsURL;
|
|
||||||
if (tenantId) return replaceTenantId(url, tenantId);
|
switch (REACT_APP_TYPE) {
|
||||||
return url;
|
case AppType.logs:
|
||||||
|
return logsURL;
|
||||||
|
case AppType.anomaly:
|
||||||
|
return serverURL || storageURL || anomalyURL;
|
||||||
|
default:
|
||||||
|
return tenantId ? replaceTenantId(url, tenantId) : url;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -66,7 +66,7 @@ export const isSupportedDuration = (str: string): Partial<Record<UnitTypeShort,
|
||||||
|
|
||||||
export const getSecondsFromDuration = (dur: string) => {
|
export const getSecondsFromDuration = (dur: string) => {
|
||||||
const shortSupportedDur = supportedDurations.map(d => d.short).join("|");
|
const shortSupportedDur = supportedDurations.map(d => d.short).join("|");
|
||||||
const regexp = new RegExp(`\\d+[${shortSupportedDur}]+`, "g");
|
const regexp = new RegExp(`\\d+(\\.\\d+)?[${shortSupportedDur}]+`, "g");
|
||||||
const durItems = dur.match(regexp) || [];
|
const durItems = dur.match(regexp) || [];
|
||||||
|
|
||||||
const durObject = durItems.reduce((prev, curr) => {
|
const durObject = durItems.reduce((prev, curr) => {
|
||||||
|
|
|
@ -14,25 +14,26 @@ export const extractFields = (metric: MetricBase["metric"]): string => {
|
||||||
.map(([key, value]) => `${key}: ${value}`).join(",");
|
.map(([key, value]) => `${key}: ${value}`).join(",");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isForecast = (metric: MetricBase["metric"]) => {
|
type ForecastMetricInfo = {
|
||||||
|
value: ForecastType | null;
|
||||||
|
group: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isForecast = (metric: MetricBase["metric"]): ForecastMetricInfo => {
|
||||||
const metricName = metric?.__name__ || "";
|
const metricName = metric?.__name__ || "";
|
||||||
const forecastRegex = new RegExp(`(${Object.values(ForecastType).join("|")})$`);
|
const forecastRegex = new RegExp(`(${Object.values(ForecastType).join("|")})$`);
|
||||||
const match = metricName.match(forecastRegex);
|
const match = metricName.match(forecastRegex);
|
||||||
const value = match && match[0] as ForecastType;
|
const value = match && match[0] as ForecastType;
|
||||||
|
const isY = /(?:^|[^a-zA-Z0-9_])y(?:$|[^a-zA-Z0-9_])/.test(metricName);
|
||||||
return {
|
return {
|
||||||
value,
|
value: isY ? ForecastType.actual : value,
|
||||||
isUpper: value === ForecastType.yhatUpper,
|
|
||||||
isLower: value === ForecastType.yhatLower,
|
|
||||||
isYhat: value === ForecastType.yhat,
|
|
||||||
isAnomaly: value === ForecastType.anomaly,
|
|
||||||
isAnomalyScore: value === ForecastType.anomalyScore,
|
|
||||||
group: extractFields(metric)
|
group: extractFields(metric)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[], isAnomaly?: boolean) => {
|
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[], isAnomalyUI?: boolean) => {
|
||||||
const colorState: {[key: string]: string} = {};
|
const colorState: {[key: string]: string} = {};
|
||||||
const maxColors = isAnomaly ? 0 : Math.min(data.length, baseContrastColors.length);
|
const maxColors = isAnomalyUI ? 0 : Math.min(data.length, baseContrastColors.length);
|
||||||
|
|
||||||
for (let i = 0; i < maxColors; i++) {
|
for (let i = 0; i < maxColors; i++) {
|
||||||
const label = getNameForMetric(data[i], alias[data[i].group - 1]);
|
const label = getNameForMetric(data[i], alias[data[i].group - 1]);
|
||||||
|
@ -40,77 +41,45 @@ export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[],
|
||||||
}
|
}
|
||||||
|
|
||||||
return (d: MetricResult, i: number): SeriesItem => {
|
return (d: MetricResult, i: number): SeriesItem => {
|
||||||
const forecast = isForecast(data[i].metric);
|
const metricInfo = isAnomalyUI ? isForecast(data[i].metric) : null;
|
||||||
const label = isAnomaly ? forecast.group : getNameForMetric(d, alias[d.group - 1]);
|
const label = isAnomalyUI ? metricInfo?.group || "" : getNameForMetric(d, alias[d.group - 1]);
|
||||||
|
|
||||||
const values = d.values.map(v => promValueToNumber(v[1]));
|
|
||||||
const { min, max, median, last } = {
|
|
||||||
min: getMinFromArray(values),
|
|
||||||
max: getMaxFromArray(values),
|
|
||||||
median: getMedianFromArray(values),
|
|
||||||
last: getLastFromArray(values),
|
|
||||||
};
|
|
||||||
|
|
||||||
let dash: number[] = [];
|
|
||||||
if (forecast.isLower || forecast.isUpper) {
|
|
||||||
dash = [10, 5];
|
|
||||||
} else if (forecast.isYhat) {
|
|
||||||
dash = [10, 2];
|
|
||||||
}
|
|
||||||
|
|
||||||
let width = 1.4;
|
|
||||||
if (forecast.isUpper || forecast.isLower) {
|
|
||||||
width = 0.7;
|
|
||||||
} else if (forecast.isYhat) {
|
|
||||||
width = 1;
|
|
||||||
} else if (forecast.isAnomaly) {
|
|
||||||
width = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let points: uPlotSeries.Points = { size: 4.2, width: 1.4 };
|
|
||||||
if (forecast.isAnomaly) {
|
|
||||||
points = { size: 8, width: 4, space: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
let stroke: uPlotSeries.Stroke = colorState[label] || getColorFromString(label);
|
|
||||||
if (isAnomaly && forecast.isAnomaly) {
|
|
||||||
stroke = anomalyColors[ForecastType.anomaly];
|
|
||||||
} else if (isAnomaly && !forecast.isAnomaly && !forecast.value) {
|
|
||||||
// TODO add stroke for training data
|
|
||||||
// const hzGrad: [number, string][] = [
|
|
||||||
// [time, anomalyColors[ForecastType.actual]],
|
|
||||||
// [time, anomalyColors[ForecastType.training]],
|
|
||||||
// [time, anomalyColors[ForecastType.actual]],
|
|
||||||
// ];
|
|
||||||
// stroke = scaleGradient("x", 0, hzGrad, true);
|
|
||||||
stroke = anomalyColors[ForecastType.actual];
|
|
||||||
} else if (forecast.value) {
|
|
||||||
stroke = forecast.value ? anomalyColors[forecast.value] : stroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
dash,
|
dash: getDashSeries(metricInfo),
|
||||||
width,
|
width: getWidthSeries(metricInfo),
|
||||||
stroke,
|
stroke: getStrokeSeries({ metricInfo, label, isAnomalyUI, colorState }),
|
||||||
points,
|
points: getPointsSeries(metricInfo),
|
||||||
spanGaps: false,
|
spanGaps: false,
|
||||||
forecast: forecast.value,
|
forecast: metricInfo?.value,
|
||||||
forecastGroup: forecast.group,
|
forecastGroup: metricInfo?.group,
|
||||||
freeFormFields: d.metric,
|
freeFormFields: d.metric,
|
||||||
show: !includesHideSeries(label, hideSeries),
|
show: !includesHideSeries(label, hideSeries),
|
||||||
scale: "1",
|
scale: "1",
|
||||||
statsFormatted: {
|
...getSeriesStatistics(d),
|
||||||
min: formatPrettyNumber(min, min, max),
|
|
||||||
max: formatPrettyNumber(max, min, max),
|
|
||||||
median: formatPrettyNumber(median, min, max),
|
|
||||||
last: formatPrettyNumber(last, min, max),
|
|
||||||
},
|
|
||||||
median: median,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSeriesStatistics = (d: MetricResult) => {
|
||||||
|
const values = d.values.map(v => promValueToNumber(v[1]));
|
||||||
|
const { min, max, median, last } = {
|
||||||
|
min: getMinFromArray(values),
|
||||||
|
max: getMaxFromArray(values),
|
||||||
|
median: getMedianFromArray(values),
|
||||||
|
last: getLastFromArray(values),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
median,
|
||||||
|
statsFormatted: {
|
||||||
|
min: formatPrettyNumber(min, min, max),
|
||||||
|
max: formatPrettyNumber(max, min, max),
|
||||||
|
median: formatPrettyNumber(median, min, max),
|
||||||
|
last: formatPrettyNumber(last, min, max),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const getLegendItem = (s: SeriesItem, group: number): LegendItemType => ({
|
export const getLegendItem = (s: SeriesItem, group: number): LegendItemType => ({
|
||||||
group,
|
group,
|
||||||
label: s.label || "",
|
label: s.label || "",
|
||||||
|
@ -121,10 +90,16 @@ export const getLegendItem = (s: SeriesItem, group: number): LegendItemType => (
|
||||||
median: s.median,
|
median: s.median,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getHideSeries = ({ hideSeries, legend, metaKey, series }: HideSeriesArgs): string[] => {
|
export const getHideSeries = ({ hideSeries, legend, metaKey, series, isAnomalyView }: HideSeriesArgs): string[] => {
|
||||||
const { label } = legend;
|
const { label } = legend;
|
||||||
const include = includesHideSeries(label, hideSeries);
|
const include = includesHideSeries(label, hideSeries);
|
||||||
const labels = series.map(s => s.label || "");
|
const labels = series.map(s => s.label || "");
|
||||||
|
|
||||||
|
// if anomalyView is true, always return all series except the one specified by `label`
|
||||||
|
if (isAnomalyView) {
|
||||||
|
return labels.filter(l => l !== label);
|
||||||
|
}
|
||||||
|
|
||||||
if (metaKey) {
|
if (metaKey) {
|
||||||
return include ? hideSeries.filter(l => l !== label) : [...hideSeries, label];
|
return include ? hideSeries.filter(l => l !== label) : [...hideSeries, label];
|
||||||
} else if (hideSeries.length) {
|
} else if (hideSeries.length) {
|
||||||
|
@ -172,3 +147,71 @@ export const addSeries = (u: uPlot, series: uPlotSeries[], spanGaps = false) =>
|
||||||
u.addSeries(s);
|
u.addSeries(s);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
const getDashSeries = (metricInfo: ForecastMetricInfo | null): number[] => {
|
||||||
|
const isLower = metricInfo?.value === ForecastType.yhatLower;
|
||||||
|
const isUpper = metricInfo?.value === ForecastType.yhatUpper;
|
||||||
|
const isYhat = metricInfo?.value === ForecastType.yhat;
|
||||||
|
|
||||||
|
if (isLower || isUpper) {
|
||||||
|
return [10, 5];
|
||||||
|
} else if (isYhat) {
|
||||||
|
return [10, 2];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWidthSeries = (metricInfo: ForecastMetricInfo | null): number => {
|
||||||
|
const isLower = metricInfo?.value === ForecastType.yhatLower;
|
||||||
|
const isUpper = metricInfo?.value === ForecastType.yhatUpper;
|
||||||
|
const isYhat = metricInfo?.value === ForecastType.yhat;
|
||||||
|
const isAnomalyMetric = metricInfo?.value === ForecastType.anomaly;
|
||||||
|
|
||||||
|
if (isUpper || isLower) {
|
||||||
|
return 0.7;
|
||||||
|
} else if (isYhat) {
|
||||||
|
return 1;
|
||||||
|
} else if (isAnomalyMetric) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1.4;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPointsSeries = (metricInfo: ForecastMetricInfo | null): uPlotSeries.Points => {
|
||||||
|
const isAnomalyMetric = metricInfo?.value === ForecastType.anomaly;
|
||||||
|
|
||||||
|
if (isAnomalyMetric) {
|
||||||
|
return { size: 8, width: 4, space: 0 };
|
||||||
|
}
|
||||||
|
return { size: 4.2, width: 1.4 };
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetStrokeSeriesArgs = {
|
||||||
|
metricInfo: ForecastMetricInfo | null,
|
||||||
|
label: string,
|
||||||
|
colorState: {[p: string]: string},
|
||||||
|
isAnomalyUI?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStrokeSeries = ({ metricInfo, label, isAnomalyUI, colorState }: GetStrokeSeriesArgs): uPlotSeries.Stroke => {
|
||||||
|
const stroke: uPlotSeries.Stroke = colorState[label] || getColorFromString(label);
|
||||||
|
const isAnomalyMetric = metricInfo?.value === ForecastType.anomaly;
|
||||||
|
|
||||||
|
if (isAnomalyUI && isAnomalyMetric) {
|
||||||
|
return anomalyColors[ForecastType.anomaly];
|
||||||
|
} else if (isAnomalyUI && !isAnomalyMetric && !metricInfo?.value) {
|
||||||
|
// TODO add stroke for training data
|
||||||
|
// const hzGrad: [number, string][] = [
|
||||||
|
// [time, anomalyColors[ForecastType.actual]],
|
||||||
|
// [time, anomalyColors[ForecastType.training]],
|
||||||
|
// [time, anomalyColors[ForecastType.actual]],
|
||||||
|
// ];
|
||||||
|
// stroke = scaleGradient("x", 0, hzGrad, true);
|
||||||
|
return anomalyColors[ForecastType.actual];
|
||||||
|
} else if (metricInfo?.value) {
|
||||||
|
return metricInfo?.value ? anomalyColors[metricInfo?.value] : stroke;
|
||||||
|
}
|
||||||
|
return colorState[label] || getColorFromString(label);
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue