+ {displayNoOptionsText &&
{noOptionsText}
}
{foundOptions.map((option, i) =>
{
+ const { showInfoMessage } = useSnack();
+
+ const handleClickIcon = (copyValue: string) => {
+ navigator.clipboard.writeText(`<${copyValue}/>`);
+ showInfoMessage({ text: `<${copyValue}/> has been copied`, type: "success" });
+ };
+
+ const createHandlerClickIcon = (key: string) => () => {
+ handleClickIcon(key);
+ };
+
+ return (
+
+ {Object.entries(icons).map(([iconKey, icon]) => (
+
+
+ {icon()}
+
+
+ {`<${iconKey}/>`}
+
+
+ ))}
+
+ );
+};
+
+export default PreviewIcons;
diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
index 69d32fa22..5429c10cc 100644
--- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
@@ -116,15 +116,6 @@ export const ArrowDownIcon = () => (
);
-export const ArrowUpIcon = () => (
-
-);
-
export const ArrowDropDownIcon = () => (
);
+
+export const SearchIcon = () => (
+
+);
diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/style.scss b/app/vmui/packages/vmui/src/components/Main/Icons/style.scss
new file mode 100644
index 000000000..e1980bffc
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Main/Icons/style.scss
@@ -0,0 +1,53 @@
+@use "src/styles/variables" as *;
+
+.vm-preview-icons {
+ display: grid;
+ align-items: flex-start;
+ justify-content: center;
+ grid-template-columns: repeat(auto-fill, 100px);
+ gap: $padding-global;
+
+ &-item {
+ display: grid;
+ grid-template-rows: 1fr auto;
+ align-items: stretch;
+ justify-content: center;
+ gap: $padding-small;
+ height: 100px;
+ padding: $padding-global $padding-small;
+ border-radius: $border-radius-small;
+ border: 1px solid transparent;
+ cursor: pointer;
+ transition: box-shadow 200ms ease-in-out;
+
+ &:hover {
+ box-shadow: rgba(0, 0, 0, 0.16) 0 1px 4px;
+ }
+
+ &:active &__svg {
+ transform: scale(0.9);
+ }
+
+ &__name {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ text-align: center;
+ font-size: $font-size-small;
+ line-height: 2;
+ }
+
+ &__svg {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ transition: transform 100ms ease-out;
+
+ svg {
+ width: auto;
+ height: 24px
+ }
+ }
+ }
+}
diff --git a/app/vmui/packages/vmui/src/components/Main/Modal/style.scss b/app/vmui/packages/vmui/src/components/Main/Modal/style.scss
index a4e480ed8..983d08daa 100644
--- a/app/vmui/packages/vmui/src/components/Main/Modal/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Modal/style.scss
@@ -1,4 +1,4 @@
-@import 'src/styles/variables';
+@use "src/styles/variables" as *;
$padding-modal: 22px;
diff --git a/app/vmui/packages/vmui/src/components/Main/Popper/Popper.tsx b/app/vmui/packages/vmui/src/components/Main/Popper/Popper.tsx
index 4ed6fce0a..4f9a355fd 100644
--- a/app/vmui/packages/vmui/src/components/Main/Popper/Popper.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Popper/Popper.tsx
@@ -12,7 +12,8 @@ interface PopperProps {
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right"
animation?: string
offset?: {top: number, left: number}
- clickOutside?: boolean
+ clickOutside?: boolean,
+ fullWidth?: boolean
}
const Popper: FC
= ({
@@ -23,7 +24,8 @@ const Popper: FC = ({
onClose,
animation,
offset = { top: 6, left: 0 },
- clickOutside = true
+ clickOutside = true,
+ fullWidth
}) => {
const [isOpen, setIsOpen] = useState(true);
@@ -68,7 +70,8 @@ const Popper: FC = ({
const position = {
top: 0,
- left: 0
+ left: 0,
+ width: "auto"
};
const needAlignRight = placement === "bottom-right" || placement === "top-right";
@@ -96,8 +99,10 @@ const Popper: FC = ({
if (isOverflowRight) position.left = buttonPos.right - popperSize.width - offsetLeft;
if (isOverflowLeft) position.left = buttonPos.left + offsetLeft;
+ if (fullWidth) position.width = `${buttonPos.width}px`;
+
return position;
- },[buttonRef, placement, isOpen, children]);
+ },[buttonRef, placement, isOpen, children, fullWidth]);
if (clickOutside) useClickOutside(popperRef, () => setIsOpen(false), buttonRef);
diff --git a/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx b/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
new file mode 100644
index 000000000..475e38a8b
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
@@ -0,0 +1,120 @@
+import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
+import classNames from "classnames";
+import { ArrowDropDownIcon, CloseIcon } from "../Icons";
+import TextField from "../../../components/Main/TextField/TextField";
+import { MouseEvent } from "react";
+import Autocomplete from "../Autocomplete/Autocomplete";
+import "./style.scss";
+
+interface JobSelectorProps {
+ value: string
+ list: string[]
+ label?: string
+ placeholder?: string
+ noOptionsText?: string
+ error?: string
+ clearable?: boolean
+ searchable?: boolean
+ onChange: (value: string) => void
+}
+
+const Select: FC = ({
+ value,
+ list,
+ label,
+ placeholder,
+ error,
+ noOptionsText,
+ clearable = false,
+ searchable,
+ onChange
+}) => {
+
+ const [search, setSearch] = useState("");
+ const autocompleteAnchorEl = useRef(null);
+ const [openList, setOpenList] = useState(false);
+
+ const textFieldValue = useMemo(() => openList ? search : value, [value, search, openList]);
+ const autocompleteValue = useMemo(() => !openList ? "" : search || "(.+)", [search, openList]);
+
+ const clearFocus = () => {
+ if (document.activeElement instanceof HTMLInputElement) {
+ document.activeElement.blur();
+ }
+ };
+
+ const handleCloseList = () => {
+ setOpenList(false);
+ clearFocus();
+ };
+
+ const handleFocus = () => {
+ setOpenList(true);
+ };
+
+ const handleClickJob = (job: string) => {
+ onChange(job);
+ handleCloseList();
+ };
+
+ const createHandleClick = (job: string) => (e: MouseEvent) => {
+ handleClickJob(job);
+ e.stopPropagation();
+ };
+
+ useEffect(() => {
+ setSearch("");
+ }, [openList]);
+
+ return (
+
+
+ )}
+ />
+ {clearable && (
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default Select;
diff --git a/app/vmui/packages/vmui/src/components/Main/Select/style.scss b/app/vmui/packages/vmui/src/components/Main/Select/style.scss
new file mode 100644
index 000000000..ec3d61242
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Main/Select/style.scss
@@ -0,0 +1,50 @@
+@use "src/styles/variables" as *;
+
+.vm-select {
+ &-input {
+ position: relative;
+ cursor: pointer;
+
+ input {
+ pointer-events: none;
+ }
+
+ &__icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-end;
+ margin: 0 0 0 auto;
+ transition: transform 200ms ease-in;
+
+ svg {
+ width: 14px;
+ }
+
+ &_open {
+ transform: rotate(180deg);
+ }
+ }
+
+ &__clear {
+ position: absolute;
+ right: 30px;
+ top: 8px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px $padding-small;
+ cursor: pointer;
+ transition: opacity 200ms ease-in;
+ color: $color-text-secondary;
+ border-right: $border-divider;
+
+ svg {
+ width: 12px;
+ }
+
+ &:hover {
+ opacity: 0.7;
+ }
+ }
+ }
+}
diff --git a/app/vmui/packages/vmui/src/components/Main/Switch/Switch.tsx b/app/vmui/packages/vmui/src/components/Main/Switch/Switch.tsx
index 0f6239227..2dbe7dfee 100644
--- a/app/vmui/packages/vmui/src/components/Main/Switch/Switch.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Switch/Switch.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { ReactNode } from "react";
import classNames from "classnames";
import "./style.scss";
import { FC } from "preact/compat";
@@ -7,7 +7,7 @@ interface SwitchProps {
value: boolean
color?: "primary" | "secondary" | "error"
disabled?: boolean
- label?: string
+ label?: string | ReactNode
onChange: (value: boolean) => void
}
diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
index 7e1436a2f..1cbcd8027 100644
--- a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
@@ -8,6 +8,7 @@ interface TextFieldProps {
value?: string | number
type?: HTMLInputTypeAttribute | "textarea"
error?: string
+ placeholder?: string
endIcon?: ReactNode
startIcon?: ReactNode
disabled?: boolean
@@ -16,6 +17,8 @@ interface TextFieldProps {
onChange?: (value: string) => void
onEnter?: () => void
onKeyDown?: (e: KeyboardEvent) => void
+ onFocus?: () => void
+ onBlur?: () => void
}
const TextField: FC
= ({
@@ -23,6 +26,7 @@ const TextField: FC = ({
value,
type = "text",
error = "",
+ placeholder,
endIcon,
startIcon,
disabled = false,
@@ -30,7 +34,9 @@ const TextField: FC = ({
helperText,
onChange,
onEnter,
- onKeyDown
+ onKeyDown,
+ onFocus,
+ onBlur
}) => {
const inputRef = useRef(null);
@@ -63,6 +69,14 @@ const TextField: FC = ({
fieldRef?.current?.focus && fieldRef.current.focus();
}, [fieldRef, autofocus]);
+ const handleFocus = () => {
+ onFocus && onFocus();
+ };
+
+ const handleBlur = () => {
+ onBlur && onBlur();
+ };
+
return
)}
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/ExploreMetricItem.tsx b/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/ExploreMetricItem.tsx
new file mode 100644
index 000000000..85a6d6b97
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/ExploreMetricItem.tsx
@@ -0,0 +1,80 @@
+import React, { FC, useEffect, useMemo, useState } from "preact/compat";
+import Accordion from "../../../components/Main/Accordion/Accordion";
+import ExploreMetricItemGraph from "./ExploreMetricItemGraph";
+import "./style.scss";
+import Switch from "../../../components/Main/Switch/Switch";
+import { MouseEvent } from "react";
+
+interface ExploreMetricItemProps {
+ name: string,
+ job: string,
+ instance: string
+ openMetrics: string[]
+ onOpen: (val: boolean, id: string) => void
+}
+
+const ExploreMetricItem: FC
= ({
+ name,
+ job,
+ instance,
+ openMetrics,
+ onOpen
+}) => {
+ const expanded = useMemo(() => openMetrics.includes(name), [name, openMetrics]);
+ const isCounter = useMemo(() => /_sum?|_total?|_count?/.test(name), [name]);
+ const isBucket = useMemo(() => /_bucket?/.test(name), [name]);
+
+ const [rateEnabled, setRateEnabled] = useState(isCounter);
+
+ const handleOpenAccordion = (val: boolean) => {
+ onOpen(val, name);
+ };
+
+ const handleClickRate = (e: MouseEvent) => {
+ e.stopPropagation();
+ };
+
+ useEffect(() => {
+ setRateEnabled(isCounter);
+ }, [job, expanded]);
+
+ const Title = () => (
+
+
{name}
+ {expanded && !isBucket && (
+
+ wrapped into rate()
}
+ value={rateEnabled}
+ onChange={setRateEnabled}
+ />
+
+ )}
+
+ );
+
+ return (
+
+
}
+ defaultExpanded={expanded}
+ onChange={handleOpenAccordion}
+ >
+
+
+
+ );
+};
+
+export default ExploreMetricItem;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/ExploreMetricItemGraph.tsx b/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/ExploreMetricItemGraph.tsx
new file mode 100644
index 000000000..76a3a47a1
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/ExploreMetricItemGraph.tsx
@@ -0,0 +1,111 @@
+import React, { FC, useMemo, useState } from "preact/compat";
+import { useFetchQuery } from "../../../hooks/useFetchQuery";
+import { useGraphDispatch, useGraphState } from "../../../state/graph/GraphStateContext";
+import GraphView from "../../../components/Views/GraphView/GraphView";
+import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
+import { AxisRange } from "../../../state/graph/reducer";
+import Spinner from "../../../components/Main/Spinner/Spinner";
+import Alert from "../../../components/Main/Alert/Alert";
+import Button from "../../../components/Main/Button/Button";
+import "./style.scss";
+
+interface ExploreMetricItemGraphProps {
+ name: string,
+ job: string,
+ instance: string,
+ rateEnabled: boolean,
+ isCounter: boolean,
+ isBucket: boolean,
+}
+
+const ExploreMetricItem: FC = ({
+ name,
+ job,
+ instance,
+ rateEnabled,
+ isCounter,
+ isBucket
+}) => {
+ const { customStep, yaxis } = useGraphState();
+ const { period } = useTimeState();
+
+ const graphDispatch = useGraphDispatch();
+ const timeDispatch = useTimeDispatch();
+
+ const [showAllSeries, setShowAllSeries] = useState(false);
+
+ const query = useMemo(() => {
+ const params = Object.entries({ job, instance })
+ .filter(val => val[1])
+ .map(([key, val]) => `${key}="${val}"`);
+
+ const base = `${name}{${params.join(",")}}`;
+ const queryBase = rateEnabled ? `rate(${base})` : base;
+
+ const queryBucket = `histogram_quantiles("quantile", 0.5, 0.99, increase(${base}[5m]))`;
+ const queryBucketWithoutInstance = `histogram_quantiles("quantile", 0.5, 0.99, sum(increase(${base}[5m])) without (instance))`;
+ const queryCounterWithoutInstance = `sum(${queryBase}) without (job)`;
+ const queryWithoutInstance = `sum(${queryBase}) without (instance)`;
+
+ const isCounterWithoutInstance = isCounter && job && !instance;
+ const isBucketWithoutInstance = isBucket && job && !instance;
+ const isWithoutInstance = !isCounter && job && !instance;
+
+ if (isCounterWithoutInstance) return queryCounterWithoutInstance;
+ if (isBucketWithoutInstance) return queryBucketWithoutInstance;
+ if (isBucket) return queryBucket;
+ if (isWithoutInstance) return queryWithoutInstance;
+ return queryBase;
+ }, [name, job, instance, rateEnabled, isCounter, isBucket]);
+
+ const { isLoading, graphData, error, warning } = useFetchQuery({
+ predefinedQuery: [query],
+ visible: true,
+ customStep,
+ showAllSeries
+ });
+
+ const setYaxisLimits = (limits: AxisRange) => {
+ graphDispatch({ type: "SET_YAXIS_LIMITS", payload: limits });
+ };
+
+ const setPeriod = ({ from, to }: {from: Date, to: Date}) => {
+ timeDispatch({ type: "SET_PERIOD", payload: { from, to } });
+ };
+
+ const handleShowAll = () => {
+ setShowAllSeries(true);
+ };
+
+ return (
+
+ {isLoading &&
}
+ {error &&
{error}}
+ {warning &&
+
+
{warning}
+
+
+ }
+ {graphData && period && (
+
+ )}
+
+ );
+};
+
+export default ExploreMetricItem;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/style.scss b/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/style.scss
new file mode 100644
index 000000000..28790dee8
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/ExploreMetricItem/style.scss
@@ -0,0 +1,36 @@
+@use "src/styles/variables" as *;
+
+.vm-explore-metrics-item {
+ border-bottom: $border-divider;
+
+ &-header {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ padding: $padding-global calc(28px + $padding-global) $padding-global $padding-global;
+
+ &__rate {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: $padding-small;
+
+ code {
+ padding: 0.2em 0.4em;
+ font-size: 85%;
+ background-color: rgba($color-black, 0.05);
+ border-radius: 6px;
+ }
+ }
+ }
+
+ &-graph {
+ padding: 0 $padding-global $padding-global;
+
+ &__warning {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ justify-content: space-between;
+ }
+ }
+}
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchInstances.ts b/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchInstances.ts
new file mode 100644
index 000000000..1f91c1880
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchInstances.ts
@@ -0,0 +1,50 @@
+import { useTimeState } from "../../../state/time/TimeStateContext";
+import { useEffect, useMemo, useState } from "preact/compat";
+import { getInstancesUrl } from "../../../api/explore-metrics";
+import { useAppState } from "../../../state/common/StateContext";
+import { ErrorTypes } from "../../../types";
+
+interface FetchInstanceReturn {
+ instances: string[],
+ isLoading: boolean,
+ error?: ErrorTypes | string,
+}
+
+export const useFetchInstances = (job: string): FetchInstanceReturn => {
+ const { serverUrl } = useAppState();
+ const { period } = useTimeState();
+
+ const [instances, setInstances] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState();
+
+ const fetchUrl = useMemo(() => getInstancesUrl(serverUrl, period, job), [serverUrl, period, job]);
+
+ useEffect(() => {
+ if (!job) return;
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(fetchUrl);
+ const resp = await response.json();
+ const data = (resp.data || []) as string[];
+ setInstances(data.sort((a, b) => a.localeCompare(b)));
+
+ if (response.ok) {
+ setError(undefined);
+ } else {
+ setError(`${resp.errorType}\r\n${resp?.error}`);
+ }
+ } catch (e) {
+ if (e instanceof Error) {
+ setError(`${e.name}: ${e.message}`);
+ }
+ }
+ setIsLoading(false);
+ };
+
+ fetchData().catch(console.error);
+ }, [fetchUrl]);
+
+ return { instances, isLoading, error };
+};
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchJobs.ts b/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchJobs.ts
new file mode 100644
index 000000000..6cadc7985
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchJobs.ts
@@ -0,0 +1,49 @@
+import { useTimeState } from "../../../state/time/TimeStateContext";
+import { useEffect, useMemo, useState } from "preact/compat";
+import { getJobsUrl } from "../../../api/explore-metrics";
+import { useAppState } from "../../../state/common/StateContext";
+import { ErrorTypes } from "../../../types";
+
+interface FetchJobsReturn {
+ jobs: string[],
+ isLoading: boolean,
+ error?: ErrorTypes | string,
+}
+
+export const useFetchJobs = (): FetchJobsReturn => {
+ const { serverUrl } = useAppState();
+ const { period } = useTimeState();
+
+ const [jobs, setJobs] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState();
+
+ const fetchUrl = useMemo(() => getJobsUrl(serverUrl, period), [serverUrl, period]);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(fetchUrl);
+ const resp = await response.json();
+ const data = (resp.data || []) as string[];
+ setJobs(data.sort((a, b) => a.localeCompare(b)));
+
+ if (response.ok) {
+ setError(undefined);
+ } else {
+ setError(`${resp.errorType}\r\n${resp?.error}`);
+ }
+ } catch (e) {
+ if (e instanceof Error) {
+ setError(`${e.name}: ${e.message}`);
+ }
+ }
+ setIsLoading(false);
+ };
+
+ fetchData().catch(console.error);
+ }, [fetchUrl]);
+
+ return { jobs, isLoading, error };
+};
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchNames.ts b/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchNames.ts
new file mode 100644
index 000000000..5775f5414
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useFetchNames.ts
@@ -0,0 +1,48 @@
+import { useEffect, useMemo, useState } from "preact/compat";
+import { getNamesUrl } from "../../../api/explore-metrics";
+import { useAppState } from "../../../state/common/StateContext";
+import { ErrorTypes } from "../../../types";
+
+interface FetchNamesReturn {
+ names: string[],
+ isLoading: boolean,
+ error?: ErrorTypes | string,
+}
+
+export const useFetchNames = (job: string, instance: string): FetchNamesReturn => {
+ const { serverUrl } = useAppState();
+
+ const [names, setNames] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState();
+
+ const fetchUrl = useMemo(() => getNamesUrl(serverUrl, job, instance), [serverUrl, job, instance]);
+
+ useEffect(() => {
+ if (!job) return;
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(fetchUrl);
+ const resp = await response.json();
+ const data = (resp.data || []) as string[];
+ setNames(data.sort((a, b) => a.localeCompare(b)));
+
+ if (response.ok) {
+ setError(undefined);
+ } else {
+ setError(`${resp.errorType}\r\n${resp?.error}`);
+ }
+ } catch (e) {
+ if (e instanceof Error) {
+ setError(`${e.name}: ${e.message}`);
+ }
+ }
+ setIsLoading(false);
+ };
+
+ fetchData().catch(console.error);
+ }, [fetchUrl]);
+
+ return { names, isLoading, error };
+};
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useSetQueryParams.ts b/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useSetQueryParams.ts
new file mode 100644
index 000000000..11a42fa7d
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/hooks/useSetQueryParams.ts
@@ -0,0 +1,22 @@
+import { useEffect } from "react";
+import { compactObject } from "../../../utils/object";
+import { useTimeState } from "../../../state/time/TimeStateContext";
+import { setQueryStringWithoutPageReload } from "../../../utils/query-string";
+
+export const useSetQueryParams = () => {
+ const { duration, relativeTime, period: { date, step } } = useTimeState();
+
+ const setSearchParamsFromState = () => {
+ const params = compactObject({
+ ["g0.range_input"]: duration,
+ ["g0.end_input"]: date,
+ ["g0.step_input"]: step,
+ ["g0.relative_time"]: relativeTime
+ });
+
+ setQueryStringWithoutPageReload(params);
+ };
+
+ useEffect(setSearchParamsFromState, [duration, relativeTime, date, step]);
+ useEffect(setSearchParamsFromState, []);
+};
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/index.tsx b/app/vmui/packages/vmui/src/pages/ExploreMetrics/index.tsx
new file mode 100644
index 000000000..330315b29
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/index.tsx
@@ -0,0 +1,142 @@
+import React, { FC, useEffect, useMemo, useState } from "preact/compat";
+import { useSetQueryParams } from "./hooks/useSetQueryParams";
+import { useFetchJobs } from "./hooks/useFetchJobs";
+import Select from "../../components/Main/Select/Select";
+import Spinner from "../../components/Main/Spinner/Spinner";
+import Alert from "../../components/Main/Alert/Alert";
+import { useFetchInstances } from "./hooks/useFetchInstances";
+import { useFetchNames } from "./hooks/useFetchNames";
+import "./style.scss";
+import ExploreMetricItem from "./ExploreMetricItem/ExploreMetricItem";
+import TextField from "../../components/Main/TextField/TextField";
+import { CloseIcon, SearchIcon } from "../../components/Main/Icons";
+import Switch from "../../components/Main/Switch/Switch";
+
+const ExploreMetrics: FC = () => {
+ useSetQueryParams();
+
+ const [job, setJob] = useState("");
+ const [instance, setInstance] = useState("");
+ const [searchMetric, setSearchMetric] = useState("");
+ const [openMetrics, setOpenMetrics] = useState([]);
+ const [onlyGraphs, setOnlyGraphs] = useState(false);
+
+ const { jobs, isLoading: loadingJobs, error: errorJobs } = useFetchJobs();
+ const { instances, isLoading: loadingInstances, error: errorInstances } = useFetchInstances(job);
+ const { names, isLoading: loadingNames, error: errorNames } = useFetchNames(job, instance);
+
+ const noInstanceText = useMemo(() => job ? "" : "No instances. Please select job", [job]);
+
+ const metrics = useMemo(() => {
+ const showMetrics = onlyGraphs ? names.filter((m) => openMetrics.includes(m)) : names;
+ if (!searchMetric) return showMetrics;
+ try {
+ const regexp = new RegExp(searchMetric, "i");
+ const found = showMetrics.filter((m) => regexp.test(m));
+ return found.sort((a,b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
+ } catch (e) {
+ return [];
+ }
+ }, [names, searchMetric, openMetrics, onlyGraphs]);
+
+ const isLoading = useMemo(() => {
+ return loadingJobs || loadingInstances || loadingNames;
+ }, [loadingJobs, loadingInstances, loadingNames]);
+
+ const error = useMemo(() => {
+ return errorJobs || errorInstances || errorNames;
+ }, [errorJobs, errorInstances, errorNames]);
+
+ const handleClearSearch = () => {
+ setSearchMetric("");
+ };
+
+ const handleOpenMetric = (val: boolean, id: string) => {
+ setOpenMetrics(prev => {
+ if (!val) {
+ return prev.filter(item => item !== id);
+ }
+ if (!prev.includes(id)) {
+ return [...prev, id];
+ }
+
+ return prev;
+ });
+ };
+
+ useEffect(() => {
+ setInstance("");
+ }, [job]);
+
+ return (
+
+
+
+
}
+ endIcon={(
+
+
+
+ )}
+ />
+
+
+ {isLoading &&
}
+ {error &&
{error}}
+ {!job &&
Please select job to see list of metric names.}
+ {!metrics.length && onlyGraphs && job && (
+
+ Open graphs not found. Turn off "Show only open metrics" to see list of metric names.
+
+ )}
+
+ {metrics.map((n) => (
+
+ ))}
+
+
+ );
+};
+
+export default ExploreMetrics;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreMetrics/style.scss b/app/vmui/packages/vmui/src/pages/ExploreMetrics/style.scss
new file mode 100644
index 000000000..9d04ea57c
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreMetrics/style.scss
@@ -0,0 +1,44 @@
+@use "src/styles/variables" as *;
+
+.vm-explore-metrics {
+ display: grid;
+ align-items: flex-start;
+ gap: $padding-medium;
+
+ &-header {
+ display: grid;
+ gap: $padding-small;
+
+ &-top {
+ display: grid;
+ grid-template-columns: minmax(200px, 300px) minmax(200px, 500px) auto;
+ align-items: center;
+ gap: $padding-medium;
+
+ &__switch-graphs {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ }
+ }
+
+ &__clear-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2px;
+ cursor: pointer;
+
+ &:hover {
+ opacity: 0.7
+ }
+ }
+ }
+
+ &-body {
+ display: grid;
+ align-items: flex-start;
+ border-radius: $border-radius-small;
+ box-shadow: $box-shadow;
+ }
+}
diff --git a/app/vmui/packages/vmui/src/pages/TracePage/index.tsx b/app/vmui/packages/vmui/src/pages/TracePage/index.tsx
index 440c093ae..a3b99cd18 100644
--- a/app/vmui/packages/vmui/src/pages/TracePage/index.tsx
+++ b/app/vmui/packages/vmui/src/pages/TracePage/index.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useMemo, useState } from "preact/compat";
+import React, { FC, useEffect, useMemo, useState } from "preact/compat";
import { ChangeEvent } from "react";
import Trace from "../../components/TraceQuery/Trace";
import TracingsView from "../../components/TraceQuery/TracingsView";
@@ -10,6 +10,7 @@ import { CloseIcon } from "../../components/Main/Icons";
import Modal from "../../components/Main/Modal/Modal";
import JsonForm from "./JsonForm/JsonForm";
import { ErrorTypes } from "../../types";
+import { setQueryStringWithoutPageReload } from "../../utils/query-string";
const TracePage: FC = () => {
const [openModal, setOpenModal] = useState(false);
@@ -72,6 +73,10 @@ const TracePage: FC = () => {
handleCloseError(index);
};
+ useEffect(() => {
+ setQueryStringWithoutPageReload({});
+ }, []);
+
const UploadButtons = () => (