mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-17 00:26:36 +03:00
app/vmui: removed anomaly ui (#10316)
The vmanomaly has been moved to a separate repository. This means that the functionality related to vmanomaly is no longer needed in the app/vmui located in the VictoriaMetrics repository. This commit removes all the functionality and unnecessary abstractions related to vmanomaly from the app/vmui repository. This should help improving long-term maintenance of the code. fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9755
This commit is contained in:
@@ -14,14 +14,6 @@ vmui-build: copy-metricsql-docs vmui-package-base-image
|
||||
--entrypoint=/bin/bash \
|
||||
vmui-builder-image -c "npm install && npm run build"
|
||||
|
||||
vmui-anomaly-build: vmui-package-base-image
|
||||
docker run --rm \
|
||||
--user $(shell id -u):$(shell id -g) \
|
||||
--mount type=bind,src="$(shell pwd)/app/vmui",dst=/build \
|
||||
-w /build/packages/vmui \
|
||||
--entrypoint=/bin/bash \
|
||||
vmui-builder-image -c "npm install && npm run build:anomaly"
|
||||
|
||||
vmui-release: vmui-build
|
||||
docker build -t ${DOCKER_NAMESPACE}/vmui:latest -f app/vmui/Dockerfile-web ./app/vmui/packages/vmui
|
||||
docker tag ${DOCKER_NAMESPACE}/vmui:latest ${DOCKER_NAMESPACE}/vmui:${PKG_TAG}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
VITE_APP_TYPE=vmanomaly
|
||||
@@ -1,23 +0,0 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { IndexHtmlTransform } from "vite";
|
||||
|
||||
/**
|
||||
* Vite plugin to dynamically load index.html based on the current mode.
|
||||
* If a specific mode-based index file (e.g., index.vmanomaly.html) exists, it is used.
|
||||
* Otherwise, the default index.html is loaded.
|
||||
*/
|
||||
export default function dynamicIndexHtmlPlugin({ mode }) {
|
||||
return {
|
||||
name: "vm-dynamic-index-html",
|
||||
transformIndexHtml: {
|
||||
order: "pre",
|
||||
handler: async () => {
|
||||
try {
|
||||
return await readFile(`./index.${mode}.html`, "utf8");
|
||||
} catch (error) {
|
||||
return await readFile("./index.html", "utf8");
|
||||
}
|
||||
}
|
||||
} as IndexHtmlTransform
|
||||
};
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta name="description" content="Detect anomalies in your metrics with VictoriaMetrics Anomaly Detection UI"/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials"/>
|
||||
<!--
|
||||
Notice the use of in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>UI for VictoriaMetrics Anomaly Detection</title>
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="UI for VictoriaMetrics Anomaly Detection">
|
||||
<meta name="twitter:site" content="@https://victoriametrics.com/products/enterprise/anomaly-detection/">
|
||||
<meta name="twitter:description" content="Detect anomalies in your metrics with VictoriaMetrics Anomaly Detection UI">
|
||||
<meta name="twitter:image" content="/preview.jpg">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="UI for VictoriaMetrics Anomaly Detection">
|
||||
<meta property="og:url" content="https://victoriametrics.com/products/enterprise/anomaly-detection/">
|
||||
<meta property="og:description" content="Detect anomalies in your metrics with VictoriaMetrics Anomaly Detection UI">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,10 +7,8 @@
|
||||
"scripts": {
|
||||
"prestart": "npm run copy-metricsql-docs",
|
||||
"start": "vite",
|
||||
"start:playground": "cross-env PLAYGROUND=METRICS npm run start",
|
||||
"start:anomaly": "vite --mode vmanomaly",
|
||||
"start:playground": "cross-env PLAYGROUND=true npm run start",
|
||||
"build": "vite build",
|
||||
"build:anomaly": "vite build --mode vmanomaly",
|
||||
"lint": "eslint --output-file vmui-lint-report.json --format json 'src/**/*.{ts,tsx}'",
|
||||
"lint:local": "eslint --ext .ts,.tsx -f stylish src",
|
||||
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { FC, useState } from "preact/compat";
|
||||
import { HashRouter, Route, Routes } from "react-router-dom";
|
||||
import AppContextProvider from "./contexts/AppContextProvider";
|
||||
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
|
||||
import AnomalyLayout from "./layouts/AnomalyLayout/AnomalyLayout";
|
||||
import ExploreAnomaly from "./pages/ExploreAnomaly/ExploreAnomaly";
|
||||
import router from "./router";
|
||||
import CustomPanel from "./pages/CustomPanel";
|
||||
|
||||
const AppAnomaly: FC = () => {
|
||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||
|
||||
return <>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme}/>
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<Route
|
||||
path={"/"}
|
||||
element={<AnomalyLayout/>}
|
||||
>
|
||||
<Route
|
||||
path={"/"}
|
||||
element={<ExploreAnomaly/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.query}
|
||||
element={<CustomPanel/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default AppAnomaly;
|
||||
@@ -14,12 +14,11 @@ export type QueryGroup = {
|
||||
interface LegendProps {
|
||||
labels: LegendItemType[];
|
||||
query: string[];
|
||||
isAnomalyView?: boolean;
|
||||
isPredefinedPanel?: boolean;
|
||||
onChange: (item: LegendItemType, metaKey: boolean) => void;
|
||||
}
|
||||
|
||||
const Legend: FC<LegendProps> = ({ labels, query, isAnomalyView, isPredefinedPanel, onChange }) => {
|
||||
const Legend: FC<LegendProps> = ({ labels, query, isPredefinedPanel, onChange }) => {
|
||||
const { groupByLabel } = useLegendGroup();
|
||||
const groupSeries = useGroupSeries({ labels, query, groupByLabel });
|
||||
|
||||
@@ -33,7 +32,6 @@ const Legend: FC<LegendProps> = ({ labels, query, isAnomalyView, isPredefinedPan
|
||||
key={group}
|
||||
labels={items}
|
||||
group={group}
|
||||
isAnomalyView={isAnomalyView}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { getFromStorage } from "../../../../utils/storage";
|
||||
|
||||
export type LegendProps = {
|
||||
labels: LegendItemType[];
|
||||
isAnomalyView?: boolean;
|
||||
duplicateFields?: string[];
|
||||
onChange: (item: LegendItemType, metaKey: boolean) => void;
|
||||
}
|
||||
@@ -22,7 +21,7 @@ interface LegendGroupProps extends LegendProps {
|
||||
group: string | number;
|
||||
}
|
||||
|
||||
const LegendGroup: FC<LegendGroupProps> = ({ labels, group, isAnomalyView, onChange }) => {
|
||||
const LegendGroup: FC<LegendGroupProps> = ({ labels, group, onChange }) => {
|
||||
const { isTableView } = useLegendView();
|
||||
const { groupByLabel } = useLegendGroup();
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
@@ -81,7 +80,6 @@ const LegendGroup: FC<LegendGroupProps> = ({ labels, group, isAnomalyView, onCha
|
||||
>
|
||||
<Content
|
||||
labels={sortedLabels}
|
||||
isAnomalyView={isAnomalyView}
|
||||
duplicateFields={duplicateFields}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
@@ -13,11 +13,10 @@ import { getLabelAlias } from "../../../../../utils/metric";
|
||||
interface LegendItemProps {
|
||||
legend: LegendItemType;
|
||||
onChange?: (item: LegendItemType, metaKey: boolean) => void;
|
||||
isAnomalyView?: boolean;
|
||||
duplicateFields?: string[];
|
||||
}
|
||||
|
||||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields, isAnomalyView }) => {
|
||||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
const { hideStats } = useShowStats();
|
||||
|
||||
@@ -52,12 +51,10 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields, is
|
||||
})}
|
||||
onClick={createHandlerClick(legend)}
|
||||
>
|
||||
{!isAnomalyView && (
|
||||
<div
|
||||
className="vm-legend-item__marker"
|
||||
style={{ backgroundColor: legend.color }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="vm-legend-item__marker"
|
||||
style={{ backgroundColor: legend.color }}
|
||||
/>
|
||||
<div className="vm-legend-item-info">
|
||||
<span className="vm-legend-item-info__label">
|
||||
{legend.hasAlias && legend.label}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC } from "preact/compat";
|
||||
import LegendItem from "../LegendItem/LegendItem";
|
||||
import { LegendProps } from "../LegendGroup";
|
||||
|
||||
const LegendLines: FC<LegendProps> = ({ labels, isAnomalyView, duplicateFields, onChange }) => {
|
||||
const LegendLines: FC<LegendProps> = ({ labels, duplicateFields, onChange }) => {
|
||||
|
||||
return (
|
||||
<div className="vm-legend-item-container">
|
||||
@@ -10,7 +10,6 @@ const LegendLines: FC<LegendProps> = ({ labels, isAnomalyView, duplicateFields,
|
||||
<LegendItem
|
||||
key={legendItem.label}
|
||||
legend={legendItem}
|
||||
isAnomalyView={isAnomalyView}
|
||||
duplicateFields={duplicateFields}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { FC, useMemo } from "preact/compat";
|
||||
import { ForecastType, SeriesItem } from "../../../../types";
|
||||
import { anomalyColors } from "../../../../utils/color";
|
||||
import "./style.scss";
|
||||
|
||||
type Props = {
|
||||
series: SeriesItem[];
|
||||
};
|
||||
|
||||
const titles: Partial<Record<ForecastType, string>> = {
|
||||
[ForecastType.yhat]: "yhat",
|
||||
[ForecastType.yhatLower]: "yhat_upper - yhat_lower",
|
||||
[ForecastType.yhatUpper]: "yhat_upper - yhat_lower",
|
||||
[ForecastType.anomaly]: "anomalies",
|
||||
[ForecastType.training]: "training data",
|
||||
[ForecastType.actual]: "y"
|
||||
};
|
||||
|
||||
const LegendAnomaly: FC<Props> = ({ series }) => {
|
||||
|
||||
const uniqSeriesStyles = useMemo(() => {
|
||||
const uniqSeries = series.reduce((accumulator, currentSeries) => {
|
||||
const hasForecast = Object.prototype.hasOwnProperty.call(currentSeries, "forecast");
|
||||
const isNotUpper = currentSeries.forecast !== ForecastType.yhatUpper;
|
||||
const isUniqForecast = !accumulator.find(s => s.forecast === currentSeries.forecast);
|
||||
if (hasForecast && isUniqForecast && isNotUpper) {
|
||||
accumulator.push(currentSeries);
|
||||
}
|
||||
return accumulator;
|
||||
}, [] as SeriesItem[]);
|
||||
|
||||
const trainingSeries = {
|
||||
...uniqSeries[0],
|
||||
forecast: ForecastType.training,
|
||||
color: anomalyColors[ForecastType.training],
|
||||
};
|
||||
uniqSeries.splice(1, 0, trainingSeries);
|
||||
|
||||
return uniqSeries.map(s => ({
|
||||
...s,
|
||||
color: typeof s.stroke === "string" ? s.stroke : anomalyColors[s.forecast || ForecastType.actual],
|
||||
}));
|
||||
}, [series]);
|
||||
|
||||
return <>
|
||||
<div className="vm-legend-anomaly">
|
||||
{/* TODO: remove .filter() after the correct training data has been added */}
|
||||
{uniqSeriesStyles.filter(f => f.forecast !== ForecastType.training).map((s, i) => (
|
||||
<div
|
||||
key={`${i}_${s.forecast}`}
|
||||
className="vm-legend-anomaly-item"
|
||||
>
|
||||
<svg>
|
||||
{s.forecast === ForecastType.anomaly ? (
|
||||
<circle
|
||||
cx="15"
|
||||
cy="7"
|
||||
r="4"
|
||||
fill={s.color}
|
||||
stroke={s.color}
|
||||
strokeWidth="1.4"
|
||||
/>
|
||||
) : (
|
||||
<line
|
||||
x1="0"
|
||||
y1="7"
|
||||
x2="30"
|
||||
y2="7"
|
||||
stroke={s.color}
|
||||
strokeWidth={s.width || 1}
|
||||
strokeDasharray={s.dash?.join(",")}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<div className="vm-legend-anomaly-item__title">{titles[s.forecast || ForecastType.actual]}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default LegendAnomaly;
|
||||
@@ -1,23 +0,0 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-legend-anomaly {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: calc($padding-large * 2);
|
||||
cursor: default;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $padding-small;
|
||||
|
||||
svg {
|
||||
width: 30px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
getRangeY,
|
||||
getScales,
|
||||
handleDestroy,
|
||||
setBand,
|
||||
setSelect
|
||||
} from "../../../../utils/uplot";
|
||||
import { MetricResult } from "../../../../api/types";
|
||||
@@ -40,7 +39,6 @@ export interface LineChartProps {
|
||||
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
|
||||
layoutSize: ElementSize;
|
||||
height?: number;
|
||||
isAnomalyView?: boolean;
|
||||
spanGaps?: boolean;
|
||||
showAllPoints?: boolean;
|
||||
}
|
||||
@@ -55,7 +53,6 @@ const LineChart: FC<LineChartProps> = ({
|
||||
setPeriod,
|
||||
layoutSize,
|
||||
height,
|
||||
isAnomalyView,
|
||||
spanGaps = false,
|
||||
showAllPoints = false,
|
||||
}) => {
|
||||
@@ -75,7 +72,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||
seriesFocus,
|
||||
setCursor,
|
||||
resetTooltips
|
||||
} = useLineTooltip({ u: uPlotInst, metrics, series, unit, isAnomalyView });
|
||||
} = useLineTooltip({ u: uPlotInst, metrics, series, unit });
|
||||
|
||||
const options: uPlotOptions = {
|
||||
...getDefaultOptions({ width: layoutSize.width, height }),
|
||||
@@ -111,7 +108,6 @@ const LineChart: FC<LineChartProps> = ({
|
||||
if (!uPlotInst) return;
|
||||
delSeries(uPlotInst);
|
||||
addSeries(uPlotInst, series, spanGaps, showAllPoints);
|
||||
setBand(uPlotInst, series);
|
||||
uPlotInst.redraw();
|
||||
}, [series, spanGaps, showAllPoints]);
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { FC, useState } from "preact/compat";
|
||||
import Button from "../Main/Button/Button";
|
||||
import TextField from "../Main/TextField/TextField";
|
||||
import Modal from "../Main/Modal/Modal";
|
||||
import Spinner from "../Main/Spinner/Spinner";
|
||||
import { DownloadIcon, ErrorIcon } from "../Main/Icons";
|
||||
import useBoolean from "../../hooks/useBoolean";
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import { useAppState } from "../../state/common/StateContext";
|
||||
import classNames from "classnames";
|
||||
import "./style.scss";
|
||||
import { useQueryState } from "../../state/query/QueryStateContext";
|
||||
import { useTimeState } from "../../state/time/TimeStateContext";
|
||||
import { getStepFromDuration } from "../../utils/time";
|
||||
|
||||
const AnomalyConfig: FC = () => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const {
|
||||
value: isModalOpen,
|
||||
setTrue: setOpenModal,
|
||||
setFalse: setCloseModal,
|
||||
} = useBoolean(false);
|
||||
|
||||
const { query } = useQueryState();
|
||||
const { period } = useTimeState();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [textConfig, setTextConfig] = useState<string>("");
|
||||
const [downloadUrl, setDownloadUrl] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchConfig = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const queryParam = encodeURIComponent(query[0] || "");
|
||||
const stepParam = encodeURIComponent(period.step || getStepFromDuration(period.end - period.start, false));
|
||||
|
||||
const url = `${serverUrl}/api/vmanomaly/config.yaml?query=${queryParam}&step=${stepParam}`;
|
||||
const response = await fetch(url);
|
||||
const contentType = response.headers.get("Content-Type");
|
||||
if (!response.ok) {
|
||||
const bodyText = await response.text();
|
||||
setError(` ${response.status} ${response.statusText}: ${bodyText}`);
|
||||
} else if (contentType == "application/yaml") {
|
||||
const blob = await response.blob();
|
||||
const yamlAsString = await blob.text();
|
||||
setTextConfig(yamlAsString);
|
||||
setDownloadUrl(URL.createObjectURL(blob));
|
||||
} else {
|
||||
setError("Response Content-Type is not YAML, does `Server URL` point to VMAnomaly server?");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError(String(error));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setOpenModal();
|
||||
setError("");
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
setTextConfig("");
|
||||
setDownloadUrl("");
|
||||
return fetchConfig();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
onClick={handleOpenModal}
|
||||
>
|
||||
Open Config
|
||||
</Button>
|
||||
{isModalOpen && (
|
||||
<Modal
|
||||
title="Download config"
|
||||
onClose={setCloseModal}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-anomaly-config": true,
|
||||
"vm-anomaly-config_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
{isLoading && (
|
||||
<Spinner
|
||||
containerStyles={{ position: "relative" }}
|
||||
message={"Loading config..."}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && error && (
|
||||
<div className="vm-anomaly-config-error">
|
||||
<div className="vm-anomaly-config-error__icon"><ErrorIcon/></div>
|
||||
<h3 className="vm-anomaly-config-error__title">Cannot download config</h3>
|
||||
<p className="vm-anomaly-config-error__text">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && textConfig && (
|
||||
<TextField
|
||||
value={textConfig}
|
||||
label={"config.yaml"}
|
||||
type="textarea"
|
||||
disabled={true}
|
||||
/>
|
||||
)}
|
||||
<div className="vm-anomaly-config-footer">
|
||||
{downloadUrl && (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download={"config.yaml"}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<DownloadIcon/>}
|
||||
>
|
||||
download
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnomalyConfig;
|
||||
@@ -1,61 +0,0 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-anomaly-config {
|
||||
display: grid;
|
||||
grid-template-rows: calc(($vh * 70) - 78px - ($padding-medium*3)) auto;
|
||||
gap: $padding-global;
|
||||
min-width: 400px;
|
||||
max-width: 80vw;
|
||||
min-height: 300px;
|
||||
|
||||
&_mobile {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-height: 100%;
|
||||
grid-template-rows: calc(($vh * 100) - 78px - ($padding-global*3)) auto;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 900px;
|
||||
}
|
||||
|
||||
&-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: $padding-small;
|
||||
text-align: center;
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-bottom: $padding-small;
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $font-size-medium;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__text {
|
||||
max-width: 700px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@ import {
|
||||
getMinMaxBuffer,
|
||||
getTimeSeries,
|
||||
} from "../../../utils/uplot";
|
||||
import { TimeParams, SeriesItem, LegendItemType } from "../../../types";
|
||||
import { TimeParams, LegendItemType } from "../../../types";
|
||||
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
|
||||
import { getMathStats } from "../../../utils/math";
|
||||
import classNames from "classnames";
|
||||
@@ -23,8 +23,6 @@ import { promValueToNumber } from "../../../utils/metric";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useElementSize from "../../../hooks/useElementSize";
|
||||
import { ChartTooltipProps } from "../../Chart/ChartTooltip/ChartTooltip";
|
||||
import LegendAnomaly from "../../Chart/Line/LegendAnomaly/LegendAnomaly";
|
||||
import { groupByMultipleKeys } from "../../../utils/array";
|
||||
import { useGraphDispatch } from "../../../state/graph/GraphStateContext";
|
||||
import { sameTs } from "../../../utils/time";
|
||||
import { useLocation } from "react-router-dom";
|
||||
@@ -44,7 +42,6 @@ export interface GraphViewProps {
|
||||
fullWidth?: boolean;
|
||||
height?: number;
|
||||
isHistogram?: boolean;
|
||||
isAnomalyView?: boolean;
|
||||
isPredefinedPanel?: boolean;
|
||||
spanGaps?: boolean;
|
||||
showAllPoints?: boolean;
|
||||
@@ -64,7 +61,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
fullWidth = true,
|
||||
height,
|
||||
isHistogram,
|
||||
isAnomalyView,
|
||||
isPredefinedPanel,
|
||||
spanGaps,
|
||||
showAllPoints
|
||||
@@ -89,8 +85,8 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
const [legendValue, setLegendValue] = useState<ChartTooltipProps | null>(null);
|
||||
|
||||
const getSeriesItem = useMemo(() => {
|
||||
return getSeriesItemContext(data, hideSeries, alias, showAllPoints, isAnomalyView, isRawQuery);
|
||||
}, [data, hideSeries, alias, showAllPoints, isAnomalyView, isRawQuery]);
|
||||
return getSeriesItemContext(data, hideSeries, alias, showAllPoints, isRawQuery);
|
||||
}, [data, hideSeries, alias, showAllPoints, isRawQuery]);
|
||||
|
||||
const setLimitsYaxis = (minVal: number, maxVal: number) => {
|
||||
let min = Number.isFinite(minVal) ? minVal : 0;
|
||||
@@ -102,7 +98,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
};
|
||||
|
||||
const onChangeLegend = (legend: LegendItemType, metaKey: boolean) => {
|
||||
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series, isAnomalyView }));
|
||||
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
|
||||
};
|
||||
|
||||
const prepareHistogramData = (data: (number | null)[][]) => {
|
||||
@@ -127,20 +123,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
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(() => {
|
||||
const dLen = data.length;
|
||||
|
||||
@@ -155,7 +137,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
|
||||
for (let i = 0; i < dLen; i++) {
|
||||
const d = data[i];
|
||||
const seriesItem = getSeriesItem(d, i);
|
||||
const seriesItem = getSeriesItem(d);
|
||||
tempSeries[i + 1] = seriesItem;
|
||||
tempLegend[i] = getLegendItem(seriesItem, d.group);
|
||||
|
||||
@@ -206,7 +188,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
const avg = Math.abs(Number(avgRaw));
|
||||
const range = getMinMaxBuffer(min, max);
|
||||
const rangeStep = Math.abs(range[1] - range[0]);
|
||||
const needStabilize = (avg > rangeStep * 1e10) && !isAnomalyView;
|
||||
const needStabilize = (avg > rangeStep * 1e10);
|
||||
|
||||
return needStabilize ? results.fill(avg) : results;
|
||||
});
|
||||
@@ -214,13 +196,11 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
timeDataSeries.unshift(timeSeries);
|
||||
|
||||
const result = isHistogram ? prepareHistogramData(timeDataSeries) : timeDataSeries;
|
||||
const legend = prepareAnomalyLegend(tempLegend);
|
||||
|
||||
setLimitsYaxis(minVal, maxVal);
|
||||
setDataChart(result as uPlotData);
|
||||
setSeries(tempSeries);
|
||||
setLegend(legend);
|
||||
isAnomalyView && setHideSeries(legend.map(s => s.label || "").slice(1));
|
||||
setLegend(tempLegend);
|
||||
}, [data, timezone, isHistogram, currentStep, isRawQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -232,13 +212,13 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
|
||||
for (let i = 0; i < dLen; i++) {
|
||||
const d = data[i];
|
||||
const seriesItem = getSeriesItem(d, i);
|
||||
const seriesItem = getSeriesItem(d);
|
||||
tempSeries[i + 1] = seriesItem;
|
||||
tempLegend[i] = getLegendItem(seriesItem, d.group);
|
||||
}
|
||||
|
||||
setSeries(tempSeries);
|
||||
setLegend(prepareAnomalyLegend(tempLegend));
|
||||
setLegend(tempLegend);
|
||||
}, [hideSeries]);
|
||||
|
||||
const hasTimeData = dataChart[0]?.length > 0;
|
||||
@@ -281,7 +261,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
setPeriod={setPeriod}
|
||||
layoutSize={containerSize}
|
||||
height={height}
|
||||
isAnomalyView={isAnomalyView}
|
||||
spanGaps={spanGaps}
|
||||
showAllPoints={isRawQuery ? true : showAllPoints}
|
||||
/>
|
||||
@@ -298,12 +277,10 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
onChangeLegend={setLegendValue}
|
||||
/>
|
||||
)}
|
||||
{isAnomalyView && showLegend && (<LegendAnomaly series={series as SeriesItem[]}/>)}
|
||||
{!isHistogram && showLegend && (
|
||||
<Legend
|
||||
labels={legend}
|
||||
query={query}
|
||||
isAnomalyView={isAnomalyView}
|
||||
onChange={onChangeLegend}
|
||||
isPredefinedPanel={isPredefinedPanel}
|
||||
/>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export enum AppType {
|
||||
victoriametrics = "victoriametrics",
|
||||
vmanomaly = "vmanomaly",
|
||||
}
|
||||
|
||||
export const APP_TYPE = import.meta.env.VITE_APP_TYPE;
|
||||
export const APP_TYPE_VM = APP_TYPE === AppType.victoriametrics;
|
||||
export const APP_TYPE_ANOMALY = APP_TYPE === AppType.vmanomaly;
|
||||
@@ -13,10 +13,9 @@ interface LineTooltipHook {
|
||||
metrics: MetricResult[];
|
||||
series: uPlotSeries[];
|
||||
unit?: string;
|
||||
isAnomalyView?: boolean;
|
||||
}
|
||||
|
||||
const useLineTooltip = ({ u, metrics, series, unit, isAnomalyView }: LineTooltipHook) => {
|
||||
const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
||||
@@ -79,7 +78,7 @@ const useLineTooltip = ({ u, metrics, series, unit, isAnomalyView }: LineTooltip
|
||||
point,
|
||||
u: u,
|
||||
id: `${seriesIdx}_${dataIdx}`,
|
||||
title: groups.size > 1 && !isAnomalyView ? `Query ${group}` : "",
|
||||
title: groups.size > 1 ? `Query ${group}` : "",
|
||||
dates: [date ? dayjs(date * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT) : "-"],
|
||||
value: formatPrettyNumber(value, min, max),
|
||||
info: getMetricName(metricItem, seriesItem),
|
||||
@@ -87,7 +86,7 @@ const useLineTooltip = ({ u, metrics, series, unit, isAnomalyView }: LineTooltip
|
||||
marker: `${seriesItem?.stroke}`,
|
||||
duplicateCount,
|
||||
};
|
||||
}, [u, tooltipIdx, metrics, series, unit, isAnomalyView]);
|
||||
}, [u, tooltipIdx, metrics, series, unit]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!showTooltip) return;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useAppDispatch, useAppState } from "../state/common/StateContext";
|
||||
import { useEffect, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../types";
|
||||
import { APP_TYPE_VM } from "../constants/appType";
|
||||
|
||||
const useFetchAppConfig = () => {
|
||||
const { serverUrl } = useAppState();
|
||||
@@ -12,7 +11,6 @@ const useFetchAppConfig = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAppConfig = async () => {
|
||||
if (!APP_TYPE_VM) return;
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useTimeDispatch } from "../state/time/TimeStateContext";
|
||||
import { getFromStorage } from "../utils/storage";
|
||||
import dayjs from "dayjs";
|
||||
import { getBrowserTimezone } from "../utils/time";
|
||||
import { APP_TYPE_VM } from "../constants/appType";
|
||||
|
||||
const disabledDefaultTimezone = Boolean(getFromStorage("DISABLED_DEFAULT_TIMEZONE"));
|
||||
|
||||
@@ -29,7 +28,7 @@ const useFetchDefaultTimezone = () => {
|
||||
};
|
||||
|
||||
const fetchDefaultTimezone = async () => {
|
||||
if (!serverUrl || !APP_TYPE_VM) return;
|
||||
if (!serverUrl) return;
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import { isHistogramData } from "../utils/metric";
|
||||
import { useGraphState } from "../state/graph/GraphStateContext";
|
||||
import { getStepFromDuration } from "../utils/time";
|
||||
import { getQueryStringValue } from "../utils/query-string";
|
||||
import { APP_TYPE_ANOMALY } from "../constants/appType";
|
||||
|
||||
interface FetchQueryParams {
|
||||
predefinedQuery?: string[]
|
||||
@@ -135,7 +134,7 @@ export const useFetchQuery = ({
|
||||
}
|
||||
|
||||
const preventChangeType = !!getQueryStringValue("display_mode", null);
|
||||
isHistogramResult = !APP_TYPE_ANOMALY && isDisplayChart && !preventChangeType && isHistogramData(resp.data.result);
|
||||
isHistogramResult = isDisplayChart && !preventChangeType && isHistogramData(resp.data.result);
|
||||
seriesLimit = isHistogramResult ? Infinity : defaultLimit;
|
||||
const freeTempSize = Math.max(0, seriesLimit - tempData.length);
|
||||
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
|
||||
|
||||
@@ -3,20 +3,9 @@ import "./constants/dayjsPlugins";
|
||||
import App from "./App";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import "./styles/style.scss";
|
||||
import { APP_TYPE, AppType } from "./constants/appType";
|
||||
import AppAnomaly from "./AppAnomaly";
|
||||
|
||||
const getAppComponent = () => {
|
||||
switch (APP_TYPE) {
|
||||
case AppType.vmanomaly:
|
||||
return <AppAnomaly/>;
|
||||
default:
|
||||
return <App/>;
|
||||
}
|
||||
};
|
||||
|
||||
const root = document.getElementById("root");
|
||||
if (root) render(getAppComponent(), root);
|
||||
if (root) render(<App/>, root);
|
||||
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import Header from "../Header/Header";
|
||||
import { FC, useEffect } from "preact/compat";
|
||||
import { Outlet, useSearchParams } from "react-router-dom";
|
||||
import qs from "qs";
|
||||
import "../MainLayout/style.scss";
|
||||
import { getAppModeEnable } from "../../utils/app-mode";
|
||||
import classNames from "classnames";
|
||||
import Footer from "../Footer/Footer";
|
||||
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
|
||||
|
||||
const AnomalyLayout: FC = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
useFetchDefaultTimezone();
|
||||
|
||||
// for support old links with search params
|
||||
const redirectSearchToHashParams = () => {
|
||||
const { search, href } = window.location;
|
||||
if (search) {
|
||||
const query = qs.parse(search, { ignoreQueryPrefix: true });
|
||||
Object.entries(query).forEach(([key, value]) => searchParams.set(key, value as string));
|
||||
setSearchParams(searchParams);
|
||||
window.location.search = "";
|
||||
}
|
||||
const newHref = href.replace(/\/\?#\//, "/#/");
|
||||
if (newHref !== href) window.location.replace(newHref);
|
||||
};
|
||||
|
||||
useEffect(redirectSearchToHashParams, []);
|
||||
|
||||
return <section className="vm-container">
|
||||
<Header controlsComponent={ControlsAnomalyLayout}/>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-container-body": true,
|
||||
"vm-container-body_mobile": isMobile,
|
||||
"vm-container-body_app": appModeEnable
|
||||
})}
|
||||
>
|
||||
<Outlet/>
|
||||
</div>
|
||||
{!appModeEnable && <Footer/>}
|
||||
</section>;
|
||||
};
|
||||
|
||||
export default AnomalyLayout;
|
||||
@@ -1,43 +0,0 @@
|
||||
import { FC } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import TenantsConfiguration
|
||||
from "../../components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
||||
import StepConfigurator from "../../components/Configurators/StepConfigurator/StepConfigurator";
|
||||
import { TimeSelector } from "../../components/Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
|
||||
import CardinalityDatePicker from "../../components/Configurators/CardinalityDatePicker/CardinalityDatePicker";
|
||||
import { ExecutionControls } from "../../components/Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
|
||||
import GlobalSettings from "../../components/Configurators/GlobalSettings/GlobalSettings";
|
||||
import ShortcutKeys from "../../components/Main/ShortcutKeys/ShortcutKeys";
|
||||
import { ControlsProps } from "../Header/HeaderControls/HeaderControls";
|
||||
|
||||
const ControlsAnomalyLayout: FC<ControlsProps> = ({
|
||||
displaySidebar,
|
||||
isMobile,
|
||||
headerSetup,
|
||||
accountIds,
|
||||
closeModal,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-header-controls": true,
|
||||
"vm-header-controls_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds || []}/>}
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls
|
||||
tooltip={headerSetup?.executionControls?.tooltip}
|
||||
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
|
||||
closeModal={closeModal}
|
||||
/>}
|
||||
<GlobalSettings/>
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlsAnomalyLayout;
|
||||
@@ -2,7 +2,7 @@ import { FC, useMemo } from "preact/compat";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import router from "../../router";
|
||||
import { getAppModeEnable, getAppModeParams } from "../../utils/app-mode";
|
||||
import { LogoAnomalyIcon, LogoIcon } from "../../components/Main/Icons";
|
||||
import { LogoIcon } from "../../components/Main/Icons";
|
||||
import { getCssVariable } from "../../utils/theme";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
@@ -13,19 +13,10 @@ import HeaderControls, { ControlsProps } from "./HeaderControls/HeaderControls";
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import useWindowSize from "../../hooks/useWindowSize";
|
||||
import { ComponentType } from "react";
|
||||
import { APP_TYPE, AppType } from "../../constants/appType";
|
||||
|
||||
export interface HeaderProps {
|
||||
controlsComponent: ComponentType<ControlsProps>
|
||||
}
|
||||
const Logo = () => {
|
||||
switch (APP_TYPE) {
|
||||
case AppType.vmanomaly:
|
||||
return <LogoAnomalyIcon/>;
|
||||
default:
|
||||
return <LogoIcon/>;
|
||||
}
|
||||
};
|
||||
|
||||
const Header: FC<HeaderProps> = ({ controlsComponent }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
@@ -75,7 +66,7 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
|
||||
onClick={onClickLogo}
|
||||
style={{ color }}
|
||||
>
|
||||
{<Logo/>}
|
||||
{<LogoIcon/>}
|
||||
</div>
|
||||
|
||||
{displaySidebar ? (
|
||||
|
||||
@@ -13,10 +13,9 @@ type Props = {
|
||||
isHistogram: boolean;
|
||||
graphData: MetricResult[];
|
||||
controlsRef: RefObject<HTMLDivElement>;
|
||||
isAnomalyView?: boolean;
|
||||
}
|
||||
|
||||
const GraphTab: FC<Props> = ({ isHistogram, graphData, controlsRef, isAnomalyView }) => {
|
||||
const GraphTab: FC<Props> = ({ isHistogram, graphData, controlsRef }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const { customStep, yaxis, spanGaps, showAllPoints } = useGraphState();
|
||||
@@ -74,7 +73,6 @@ const GraphTab: FC<Props> = ({ isHistogram, graphData, controlsRef, isAnomalyVie
|
||||
setPeriod={setPeriod}
|
||||
height={isMobile ? window.innerHeight * 0.5 : 500}
|
||||
isHistogram={isHistogram}
|
||||
isAnomalyView={isAnomalyView}
|
||||
spanGaps={spanGaps}
|
||||
showAllPoints={showAllPoints}
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,6 @@ import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject"
|
||||
import { QueryStats } from "../../../api/types";
|
||||
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
|
||||
import QueryHistory from "../../../components/QueryHistory/QueryHistory";
|
||||
import AnomalyConfig from "../../../components/ExploreAnomaly/AnomalyConfig";
|
||||
import QueryEditorAutocomplete from "../../../components/Configurators/QueryEditor/QueryEditorAutocomplete";
|
||||
import { getUpdatedHistory } from "../../../components/QueryHistory/utils";
|
||||
|
||||
@@ -46,7 +45,6 @@ export interface QueryConfiguratorProps {
|
||||
prettify?: boolean;
|
||||
autocomplete?: boolean;
|
||||
traceQuery?: boolean;
|
||||
anomalyConfig?: boolean;
|
||||
disableCache?: boolean;
|
||||
reduceMemUsage?: boolean;
|
||||
}
|
||||
@@ -278,7 +276,6 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||
handleSelectQuery={handleSelectHistory}
|
||||
historyKey={"METRICS_QUERY_HISTORY"}
|
||||
/>
|
||||
{hideButtons?.anomalyConfig && <AnomalyConfig/>}
|
||||
{!hideButtons?.addQuery && stateQuery.length < MAX_QUERY_FIELDS && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { FC, useMemo, useRef, useState } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import { ForecastType } from "../../types";
|
||||
import { useSetQueryParams } from "../CustomPanel/hooks/useSetQueryParams";
|
||||
import QueryConfigurator from "../CustomPanel/QueryConfigurator/QueryConfigurator";
|
||||
import "../CustomPanel/style.scss";
|
||||
import { useQueryState } from "../../state/query/QueryStateContext";
|
||||
import { useFetchQuery } from "../../hooks/useFetchQuery";
|
||||
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 { promValueToNumber } from "../../utils/metric";
|
||||
|
||||
// Hardcoded to 1.0 for now; consider adding a UI slider for threshold adjustment in the future.
|
||||
const ANOMALY_SCORE_THRESHOLD = 1;
|
||||
|
||||
const ExploreAnomaly: FC = () => {
|
||||
useSetQueryParams();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const { query } = useQueryState();
|
||||
const { customStep } = useGraphState();
|
||||
|
||||
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,
|
||||
customStep,
|
||||
hideQuery,
|
||||
showAllSeries
|
||||
});
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (!graphData) return [];
|
||||
const detectedData = graphData.map(d => ({ ...isForecast(d.metric), ...d }));
|
||||
const realData = detectedData.filter(d => d.value === ForecastType.actual);
|
||||
const anomalyScoreData = detectedData.filter(d => d.value === ForecastType.anomaly);
|
||||
const anomalyData: MetricResult[] = realData.map((d) => {
|
||||
const id = extractFields(d.metric);
|
||||
const anomalyScoreDataByLabels = anomalyScoreData.find(du => extractFields(du.metric) === id);
|
||||
|
||||
return {
|
||||
group: 1,
|
||||
metric: { ...d.metric, __name__: ForecastType.anomaly },
|
||||
values: d.values.filter(([t]) => {
|
||||
if (!anomalyScoreDataByLabels) return false;
|
||||
const anomalyScore = anomalyScoreDataByLabels.values.find(([tMax]) => tMax === t) as [number, string];
|
||||
return anomalyScore && promValueToNumber(anomalyScore[1]) > ANOMALY_SCORE_THRESHOLD;
|
||||
})
|
||||
};
|
||||
});
|
||||
const filterData = detectedData.filter(d => (d.value !== ForecastType.anomaly) && d.value) as MetricResult[];
|
||||
return filterData.concat(anomalyData);
|
||||
}, [graphData]);
|
||||
|
||||
const handleRunQuery = () => {
|
||||
setHideError(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-custom-panel": true,
|
||||
"vm-custom-panel_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<QueryConfigurator
|
||||
queryErrors={!hideError ? queryErrors : []}
|
||||
setQueryErrors={setQueryErrors}
|
||||
setHideError={setHideError}
|
||||
stats={queryStats}
|
||||
onRunQuery={handleRunQuery}
|
||||
hideButtons={{
|
||||
addQuery: true,
|
||||
prettify: false,
|
||||
autocomplete: false,
|
||||
traceQuery: true,
|
||||
anomalyConfig: true,
|
||||
reduceMemUsage: true,
|
||||
}}
|
||||
/>
|
||||
{isLoading && <Spinner/>}
|
||||
{(!hideError && error) && <Alert variant="error">{error}</Alert>}
|
||||
{warning && (
|
||||
<WarningLimitSeries
|
||||
warning={warning}
|
||||
query={query}
|
||||
onChange={setShowAllSeries}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-custom-panel-body": true,
|
||||
"vm-custom-panel-body_mobile": isMobile,
|
||||
"vm-block": true,
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="vm-custom-panel-body-header"
|
||||
ref={controlsRef}
|
||||
>
|
||||
<div/>
|
||||
</div>
|
||||
{data && (
|
||||
<GraphTab
|
||||
graphData={data}
|
||||
isHistogram={false}
|
||||
controlsRef={controlsRef}
|
||||
isAnomalyView={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreAnomaly;
|
||||
@@ -3,7 +3,6 @@ import { DashboardSettings, ErrorTypes } from "../../../types";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { useDashboardsDispatch } from "../../../state/dashboards/DashboardsStateContext";
|
||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
||||
import { APP_TYPE_VM } from "../../../constants/appType";
|
||||
|
||||
const importModule = async (filename: string) => {
|
||||
const data = await fetch(`./dashboards/${filename}`);
|
||||
@@ -35,7 +34,7 @@ export const useFetchDashboards = (): {
|
||||
};
|
||||
|
||||
const fetchRemoteDashboards = async () => {
|
||||
if (!serverUrl || !APP_TYPE_VM) return;
|
||||
if (!serverUrl) return;
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { APP_TYPE, AppType } from "../constants/appType";
|
||||
|
||||
const router = {
|
||||
home: "/",
|
||||
@@ -12,7 +11,6 @@ const router = {
|
||||
activeQueries: "/active-queries",
|
||||
queryAnalyzer: "/query-analyzer",
|
||||
icons: "/icons",
|
||||
anomaly: "/anomaly",
|
||||
query: "/query",
|
||||
rawQuery: "/raw-query",
|
||||
downsamplingDebug: "/downsampling-filters-debug",
|
||||
@@ -52,23 +50,11 @@ const routerOptionsDefault = {
|
||||
},
|
||||
};
|
||||
|
||||
const getDefaultOptions = (appType: AppType) => {
|
||||
switch (appType) {
|
||||
case AppType.vmanomaly:
|
||||
return {
|
||||
title: "Anomaly exploration",
|
||||
...routerOptionsDefault,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
[router.home]: getDefaultOptions(APP_TYPE),
|
||||
[router.home]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.rawQuery]: {
|
||||
title: "Raw query",
|
||||
header: {
|
||||
@@ -148,7 +134,6 @@ export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
title: "Icons",
|
||||
header: {},
|
||||
},
|
||||
[router.anomaly]: getDefaultOptions(AppType.vmanomaly),
|
||||
[router.query]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import router, { routerOptions } from "./index";
|
||||
import router from "./index";
|
||||
|
||||
export enum NavigationItemType {
|
||||
internalLink,
|
||||
@@ -66,13 +66,3 @@ export const getDefaultNavigation = ({
|
||||
{ value: router.dashboards, hide: !showPredefinedDashboards },
|
||||
{ value: "Alerting", submenu: getAlertingNav(), hide: !showAlerting },
|
||||
];
|
||||
|
||||
/**
|
||||
* vmanomaly navigation menu
|
||||
*/
|
||||
export const getAnomalyNavigation = (): NavigationItem[] => [
|
||||
{
|
||||
label: routerOptions[router.anomaly].title,
|
||||
value: router.home,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useDashboardsState } from "../state/dashboards/DashboardsStateContext";
|
||||
import { useAppState } from "../state/common/StateContext";
|
||||
import { useMemo } from "preact/compat";
|
||||
import { processNavigationItems } from "./utils";
|
||||
import { getAnomalyNavigation, getDefaultNavigation } from "./navigation";
|
||||
import { APP_TYPE, AppType } from "../constants/appType";
|
||||
import { getDefaultNavigation } from "./navigation";
|
||||
|
||||
const useNavigationMenu = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
@@ -23,12 +22,7 @@ const useNavigationMenu = () => {
|
||||
|
||||
|
||||
const menu = useMemo(() => {
|
||||
switch (APP_TYPE) {
|
||||
case AppType.vmanomaly:
|
||||
return getAnomalyNavigation();
|
||||
default:
|
||||
return getDefaultNavigation(navigationConfig);
|
||||
}
|
||||
return getDefaultNavigation(navigationConfig);
|
||||
}, [navigationConfig]);
|
||||
|
||||
return processNavigationItems(menu);
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { Axis, Series } from "uplot";
|
||||
|
||||
export enum ForecastType {
|
||||
yhat = "yhat",
|
||||
yhatUpper = "yhat_upper",
|
||||
yhatLower = "yhat_lower",
|
||||
anomaly = "vmui_anomalies_points",
|
||||
training = "vmui_training_data",
|
||||
actual = "actual",
|
||||
anomalyScore = "anomaly_score",
|
||||
}
|
||||
|
||||
export interface SeriesItemStatsFormatted {
|
||||
min: string,
|
||||
max: string,
|
||||
@@ -20,8 +10,6 @@ export interface SeriesItem extends Series {
|
||||
freeFormFields: {[key: string]: string};
|
||||
statsFormatted: SeriesItemStatsFormatted;
|
||||
median: number;
|
||||
forecast?: ForecastType | null;
|
||||
forecastGroup?: string;
|
||||
hasAlias?: boolean;
|
||||
}
|
||||
|
||||
@@ -30,7 +18,6 @@ export interface HideSeriesArgs {
|
||||
legend: LegendItemType,
|
||||
metaKey: boolean,
|
||||
series: Series[],
|
||||
isAnomalyView?: boolean,
|
||||
}
|
||||
|
||||
export type MinMax = { min: number, max: number }
|
||||
|
||||
@@ -2,21 +2,6 @@ export const arrayEquals = (a: (string | number)[], b: (string | number)[]) => {
|
||||
return a.length === b.length && a.every((val, index) => val === b[index]);
|
||||
};
|
||||
|
||||
export function groupByMultipleKeys<T>(items: T[], keys: (keyof T)[]): { keys: string[], values: T[] }[] {
|
||||
const groups = items.reduce((result, item) => {
|
||||
const compositeKey = keys.map(key => `${String(key)}: ${item[key] || "-"}`).join("|");
|
||||
|
||||
(result[compositeKey] = result[compositeKey] || []).push(item);
|
||||
|
||||
return result;
|
||||
}, {} as { [key: string]: T[] });
|
||||
|
||||
return Object.entries(groups).map(([keyString, values]) => ({
|
||||
keys: keyString.split("|"),
|
||||
values
|
||||
}));
|
||||
}
|
||||
|
||||
export const isDecreasing = (arr: number[]): boolean => {
|
||||
if (arr.length < 2) return false;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrayRGB, ForecastType } from "../types";
|
||||
import { ArrayRGB } from "../types";
|
||||
|
||||
export const baseContrastColors = [
|
||||
"#e54040",
|
||||
@@ -21,16 +21,6 @@ export const hexToRGB = (hex: string): string => {
|
||||
return `${r}, ${g}, ${b}`;
|
||||
};
|
||||
|
||||
export const anomalyColors: Record<ForecastType, string> = {
|
||||
[ForecastType.yhatUpper]: "#7126a1",
|
||||
[ForecastType.yhatLower]: "#7126a1",
|
||||
[ForecastType.yhat]: "#da42a6",
|
||||
[ForecastType.anomaly]: "#da4242",
|
||||
[ForecastType.anomalyScore]: "#7126a1",
|
||||
[ForecastType.actual]: "#203ea9",
|
||||
[ForecastType.training]: `rgba(${hexToRGB("#203ea9")}, 0.2)`,
|
||||
};
|
||||
|
||||
export const getColorFromString = (text: string): string => {
|
||||
const SEED = 16777215;
|
||||
const FACTOR = 49979693;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getAppModeParams } from "./app-mode";
|
||||
import { APP_TYPE, AppType } from "../constants/appType";
|
||||
import { getFromStorage } from "./storage";
|
||||
|
||||
export const getDefaultURL = (u: string) => {
|
||||
@@ -9,14 +8,6 @@ export const getDefaultURL = (u: string) => {
|
||||
export const getDefaultServer = (): string => {
|
||||
const { serverURL } = getAppModeParams();
|
||||
const storageURL = getFromStorage("SERVER_URL") as string;
|
||||
const anomalyURL = `${window.location.origin}${window.location.pathname.replace(/^\/vmui/, "")}`;
|
||||
const defaultURL = getDefaultURL(window.location.href);
|
||||
const url = serverURL || storageURL || defaultURL;
|
||||
|
||||
switch (APP_TYPE) {
|
||||
case AppType.vmanomaly:
|
||||
return storageURL || anomalyURL;
|
||||
default:
|
||||
return url;
|
||||
}
|
||||
return serverURL || storageURL || defaultURL;
|
||||
};
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import uPlot, { Series as uPlotSeries } from "uplot";
|
||||
import { ForecastType, SeriesItem } from "../../types";
|
||||
import { anomalyColors, hexToRGB } from "../color";
|
||||
|
||||
export const setBand = (plot: uPlot, series: uPlotSeries[]) => {
|
||||
// First, remove any existing bands
|
||||
plot.delBand();
|
||||
|
||||
// If there aren't at least two series, we can't create a band
|
||||
if (series.length < 2) return;
|
||||
|
||||
// Cast and enrich each series item with its index
|
||||
const seriesItems = (series as SeriesItem[]).map((s, index) => ({ ...s, index }));
|
||||
|
||||
const upperSeries = seriesItems.filter(s => s.forecast === ForecastType.yhatUpper);
|
||||
const lowerSeries = seriesItems.filter(s => s.forecast === ForecastType.yhatLower);
|
||||
|
||||
// Create bands by matching upper and lower series based on their freeFormFields
|
||||
const bands = upperSeries.map((upper) => {
|
||||
const correspondingLower = lowerSeries.find(lower => lower.forecastGroup === upper.forecastGroup);
|
||||
if (!correspondingLower) return null;
|
||||
return {
|
||||
series: [upper.index, correspondingLower.index] as [number, number],
|
||||
fill: createBandFill(ForecastType.yhatUpper),
|
||||
};
|
||||
}).filter(band => band !== null) as uPlot.Band[]; // Filter out any nulls from failed matches
|
||||
|
||||
// If there are no bands to add, exit the function
|
||||
if (!bands.length) return;
|
||||
|
||||
// Add each band to the plot
|
||||
bands.forEach(band => {
|
||||
plot.addBand(band);
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to create the fill color for a band
|
||||
function createBandFill(forecastType: ForecastType): string {
|
||||
const rgb = hexToRGB(anomalyColors[forecastType]);
|
||||
return `rgba(${rgb}, 0.05)`;
|
||||
}
|
||||
@@ -5,4 +5,3 @@ export * from "./hooks";
|
||||
export * from "./instance";
|
||||
export * from "./scales";
|
||||
export * from "./series";
|
||||
export * from "./bands";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import uPlot, { Range, Scale, Scales } from "uplot";
|
||||
import { getMinMaxBuffer } from "./axes";
|
||||
import { YaxisState } from "../../state/graph/reducer";
|
||||
import { ForecastType, MinMax, SetMinMax } from "../../types";
|
||||
import { anomalyColors } from "../color";
|
||||
import { MinMax, SetMinMax } from "../../types";
|
||||
|
||||
export const getRangeX = ({ min, max }: MinMax): Range.MinMax => [min, max];
|
||||
|
||||
@@ -25,80 +24,3 @@ export const setSelect = (setPlotScale: SetMinMax) => (u: uPlot) => {
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ min, max });
|
||||
};
|
||||
|
||||
export const scaleGradient = (
|
||||
scaleKey: string,
|
||||
ori: number,
|
||||
scaleStops: [number, string][],
|
||||
discrete = false
|
||||
) => (u: uPlot): CanvasGradient | string => {
|
||||
const can = document.createElement("canvas");
|
||||
const ctx = can.getContext("2d");
|
||||
if (!ctx) return "";
|
||||
|
||||
const scale = u.scales[scaleKey];
|
||||
|
||||
// we want the stop below or at the scaleMax
|
||||
// and the stop below or at the scaleMin, else the stop above scaleMin
|
||||
let minStopIdx = 0;
|
||||
let maxStopIdx = 1;
|
||||
|
||||
for (let i = 0; i < scaleStops.length; i++) {
|
||||
const stopVal = scaleStops[i][0];
|
||||
|
||||
if (stopVal <= (scale.min || 0) || minStopIdx == null)
|
||||
minStopIdx = i;
|
||||
|
||||
maxStopIdx = i;
|
||||
|
||||
if (stopVal >= (scale.max || 1))
|
||||
break;
|
||||
}
|
||||
|
||||
if (minStopIdx == maxStopIdx)
|
||||
return scaleStops[minStopIdx][1];
|
||||
|
||||
let minStopVal = scaleStops[minStopIdx][0];
|
||||
let maxStopVal = scaleStops[maxStopIdx][0];
|
||||
|
||||
if (minStopVal == -Infinity)
|
||||
minStopVal = scale.min || 0;
|
||||
|
||||
if (maxStopVal == Infinity)
|
||||
maxStopVal = scale.max || 1;
|
||||
|
||||
const minStopPos = u.valToPos(minStopVal, scaleKey, true) || 0;
|
||||
const maxStopPos = u.valToPos(maxStopVal, scaleKey, true) || 1;
|
||||
|
||||
const range = minStopPos - maxStopPos;
|
||||
|
||||
let x0, y0, x1, y1;
|
||||
|
||||
if (ori == 1) {
|
||||
x0 = x1 = 0;
|
||||
y0 = minStopPos;
|
||||
y1 = maxStopPos;
|
||||
} else {
|
||||
y0 = y1 = 0;
|
||||
x0 = minStopPos;
|
||||
x1 = maxStopPos;
|
||||
}
|
||||
|
||||
const grd = ctx.createLinearGradient(x0, y0, x1, y1);
|
||||
|
||||
let prevColor = anomalyColors[ForecastType.actual];
|
||||
|
||||
for (let i = minStopIdx; i <= maxStopIdx; i++) {
|
||||
const s = scaleStops[i];
|
||||
|
||||
const stopPos = i == minStopIdx ? minStopPos : i == maxStopIdx ? maxStopPos : u.valToPos(s[0], scaleKey, true) | 1;
|
||||
const pct = Math.min(1, Math.max(0, (minStopPos - stopPos) / range));
|
||||
if (discrete && i > minStopIdx) {
|
||||
grd.addColorStop(pct, prevColor);
|
||||
}
|
||||
|
||||
grd.addColorStop(pct, prevColor = s[1]);
|
||||
}
|
||||
|
||||
return grd;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MetricBase, MetricResult } from "../../api/types";
|
||||
import uPlot, { Series as uPlotSeries } from "uplot";
|
||||
import { getNameForMetric, promValueToNumber } from "../metric";
|
||||
import { ForecastType, HideSeriesArgs, LegendItemType, SeriesItem } from "../../types";
|
||||
import { anomalyColors, baseContrastColors, getColorFromString } from "../color";
|
||||
import { HideSeriesArgs, LegendItemType, SeriesItem } from "../../types";
|
||||
import { baseContrastColors, getColorFromString } from "../color";
|
||||
import { getMathStats } from "../math";
|
||||
import { formatPrettyNumber } from "./helpers";
|
||||
import { drawPoints } from "./scatter";
|
||||
@@ -15,47 +15,26 @@ export const extractFields = (metric: MetricBase["metric"]): string => {
|
||||
.map(([key, value]) => `${key}: ${value}`).join(",");
|
||||
};
|
||||
|
||||
type ForecastMetricInfo = {
|
||||
value: ForecastType | null;
|
||||
group: string;
|
||||
}
|
||||
|
||||
export const isForecast = (metric: MetricBase["metric"]): ForecastMetricInfo => {
|
||||
const metricName = metric?.__name__ || "";
|
||||
const forecastRegex = new RegExp(`(${Object.values(ForecastType).join("|")})$`);
|
||||
const match = metricName.match(forecastRegex);
|
||||
const value = match && match[0] as ForecastType;
|
||||
const isY = /(?:^|[^a-zA-Z0-9_])y(?:$|[^a-zA-Z0-9_])/.test(metricName);
|
||||
return {
|
||||
value: isY ? ForecastType.actual : value,
|
||||
group: extractFields(metric)
|
||||
};
|
||||
};
|
||||
|
||||
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[], showPoints?: boolean, isAnomalyUI?: boolean, isRawQuery?: boolean) => {
|
||||
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[], showPoints?: boolean, isRawQuery?: boolean) => {
|
||||
const colorState: {[key: string]: string} = {};
|
||||
const maxColors = isAnomalyUI ? 0 : Math.min(data.length, baseContrastColors.length);
|
||||
const maxColors = Math.min(data.length, baseContrastColors.length);
|
||||
|
||||
for (let i = 0; i < maxColors; i++) {
|
||||
const label = getNameForMetric(data[i], alias[data[i].group - 1]);
|
||||
colorState[label] = baseContrastColors[i];
|
||||
}
|
||||
|
||||
return (d: MetricResult, i: number): SeriesItem => {
|
||||
const metricInfo = isAnomalyUI ? isForecast(data[i].metric) : null;
|
||||
return (d: MetricResult): SeriesItem => {
|
||||
const aliasValue = alias[d.group - 1];
|
||||
const label = isAnomalyUI ? metricInfo?.group || "" : getNameForMetric(d, aliasValue);
|
||||
const label = getNameForMetric(d, aliasValue);
|
||||
|
||||
return {
|
||||
label,
|
||||
hasAlias: Boolean(aliasValue),
|
||||
dash: getDashSeries(metricInfo),
|
||||
width: getWidthSeries(metricInfo),
|
||||
stroke: getStrokeSeries({ metricInfo, label, isAnomalyUI, colorState }),
|
||||
points: getPointsSeries(metricInfo, showPoints, isRawQuery),
|
||||
width: 1.4,
|
||||
stroke: colorState[label] || getColorFromString(label),
|
||||
points: getPointsSeries(showPoints, isRawQuery),
|
||||
spanGaps: false,
|
||||
forecast: metricInfo?.value,
|
||||
forecastGroup: metricInfo?.group,
|
||||
freeFormFields: d.metric,
|
||||
show: !includesHideSeries(label, hideSeries),
|
||||
scale: "1",
|
||||
@@ -91,16 +70,11 @@ export const getLegendItem = (s: SeriesItem, group: number): LegendItemType => (
|
||||
hasAlias: s.hasAlias || false,
|
||||
});
|
||||
|
||||
export const getHideSeries = ({ hideSeries, legend, metaKey, series, isAnomalyView }: HideSeriesArgs): string[] => {
|
||||
export const getHideSeries = ({ hideSeries, legend, metaKey, series }: HideSeriesArgs): string[] => {
|
||||
const { label } = legend;
|
||||
const include = includesHideSeries(label, hideSeries);
|
||||
const labels = series.map(getLabelForSeries);
|
||||
|
||||
// if anomalyView is true, always return all series except the one specified by `label`
|
||||
if (isAnomalyView) {
|
||||
return labels.filter(l => l !== label);
|
||||
}
|
||||
|
||||
if (metaKey) {
|
||||
return include ? hideSeries.filter(l => l !== label) : [...hideSeries, label];
|
||||
} else if (hideSeries.length) {
|
||||
@@ -128,43 +102,7 @@ export const addSeries = (u: uPlot, series: uPlotSeries[], spanGaps = false, sho
|
||||
});
|
||||
};
|
||||
|
||||
// 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, showPoints: boolean = false, isRawQuery?: boolean): uPlotSeries.Points => {
|
||||
const isAnomalyMetric = metricInfo?.value === ForecastType.anomaly;
|
||||
|
||||
if (isAnomalyMetric) {
|
||||
return { size: 8, width: 4, space: 0 };
|
||||
}
|
||||
const getPointsSeries = (showPoints: boolean = false, isRawQuery?: boolean): uPlotSeries.Points => {
|
||||
return {
|
||||
size: isRawQuery ? 0 : 4,
|
||||
width: 0,
|
||||
@@ -187,31 +125,3 @@ const filterPoints = (self: uPlot, seriesIdx: number): number[] | null => {
|
||||
|
||||
return indices;
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -2,44 +2,40 @@ import * as path from "path";
|
||||
|
||||
import { defineConfig, ProxyOptions } from "vite";
|
||||
import preact from "@preact/preset-vite";
|
||||
import dynamicIndexHtmlPlugin from "./config/plugins/dynamicIndexHtml";
|
||||
|
||||
const getProxy = (): Record<string, ProxyOptions> | undefined => {
|
||||
const playground = process.env.PLAYGROUND;
|
||||
const playground = process.env.PLAYGROUND.toLowerCase();
|
||||
|
||||
switch (playground) {
|
||||
case "METRICS": {
|
||||
return {
|
||||
"^/(api|vmalert)/.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/prometheus",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
"/vmui/config.json": {
|
||||
target: "https://play.victoriametrics.com/select/0",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
if (playground !== "true") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
"^/(api|vmalert)/.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/prometheus",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
"/prometheus/vmui/config.json": {
|
||||
target: "https://play.victoriametrics.com/select/0",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
base: "",
|
||||
plugins: [preact(), dynamicIndexHtmlPlugin({ mode })],
|
||||
plugins: [preact()],
|
||||
assetsInclude: ["**/*.md"],
|
||||
server: {
|
||||
open: true,
|
||||
|
||||
Reference in New Issue
Block a user