mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-10 15:14:09 +00:00
vmui: improve Explore metrics
(#3598)
* feat: add multiple select * feat: improve explore interface * app/vmselect/vmui: `make vmui-update` Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
0e1f0ade31
commit
2460e0f51e
40 changed files with 776 additions and 403 deletions
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.9a291a47.css",
|
||||
"main.js": "./static/js/main.e3ded72d.js",
|
||||
"main.css": "./static/css/main.e9e7cdb7.css",
|
||||
"main.js": "./static/js/main.d34bbb5e.js",
|
||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.9a291a47.css",
|
||||
"static/js/main.e3ded72d.js"
|
||||
"static/css/main.e9e7cdb7.css",
|
||||
"static/js/main.d34bbb5e.js"
|
||||
]
|
||||
}
|
|
@ -1 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.e3ded72d.js"></script><link href="./static/css/main.9a291a47.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.d34bbb5e.js"></script><link href="./static/css/main.e9e7cdb7.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
File diff suppressed because one or more lines are too long
1
app/vmselect/vmui/static/css/main.e9e7cdb7.css
Normal file
1
app/vmselect/vmui/static/css/main.e9e7cdb7.css
Normal file
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.d34bbb5e.js
Normal file
2
app/vmselect/vmui/static/js/main.d34bbb5e.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -30,7 +30,8 @@ export interface LineChartProps {
|
|||
series: uPlotSeries[];
|
||||
unit?: string;
|
||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
||||
container: HTMLDivElement | null
|
||||
container: HTMLDivElement | null;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
|
||||
|
@ -43,7 +44,8 @@ const LineChart: FC<LineChartProps> = ({
|
|||
yaxis,
|
||||
unit,
|
||||
setPeriod,
|
||||
container
|
||||
container,
|
||||
height
|
||||
}) => {
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
|
@ -172,6 +174,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
axes: getAxes( [{}, { scale: "1" }], unit),
|
||||
scales: { ...getScales() },
|
||||
width: layoutSize.width || 400,
|
||||
height: height || 500,
|
||||
plugins: [{ hooks: { ready: onReadyChart, setCursor, setSeries: seriesFocus } }],
|
||||
hooks: {
|
||||
setSelect: [
|
||||
|
@ -213,7 +216,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
setUPlotInst(u);
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, series, layoutSize]);
|
||||
}, [uPlotRef.current, series, layoutSize, height]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-line-chart {
|
||||
height: 500px;
|
||||
pointer-events: auto;
|
||||
|
||||
&_panning {
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
&-list {
|
||||
min-width: 600px;
|
||||
max-height: 300px;
|
||||
max-height: 200px;
|
||||
background-color: $color-background-block;
|
||||
border-radius: $border-radius-medium;
|
||||
overflow: auto;
|
||||
|
|
|
@ -128,8 +128,8 @@ export const ExecutionControls: FC = () => {
|
|||
{delayOptions.map(d => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list__item": true,
|
||||
"vm-list__item_active": d.seconds === selectedDelay.seconds
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||
})}
|
||||
key={d.seconds}
|
||||
onClick={createHandlerChange(d)}
|
||||
|
|
|
@ -19,8 +19,8 @@ const TimeDurationSelector: FC<TimeDurationSelector> = ({ relativeTime, setDurat
|
|||
{relativeTimeOptions.map(({ id, duration, until, title }) => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list__item": true,
|
||||
"vm-list__item_active": id === relativeTime
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_active": id === relativeTime
|
||||
})}
|
||||
key={id}
|
||||
onClick={createHandlerClick({ duration, until: until(), id })}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
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 GraphView from "../../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 Spinner from "../../Main/Spinner/Spinner";
|
||||
import Alert from "../../Main/Alert/Alert";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import "./style.scss";
|
||||
|
||||
interface ExploreMetricItemGraphProps {
|
||||
|
@ -15,6 +15,8 @@ interface ExploreMetricItemGraphProps {
|
|||
instance: string,
|
||||
rateEnabled: boolean,
|
||||
isBucket: boolean,
|
||||
showLegend: boolean
|
||||
height?: number
|
||||
}
|
||||
|
||||
const ExploreMetricItem: FC<ExploreMetricItemGraphProps> = ({
|
||||
|
@ -22,7 +24,9 @@ const ExploreMetricItem: FC<ExploreMetricItemGraphProps> = ({
|
|||
job,
|
||||
instance,
|
||||
rateEnabled,
|
||||
isBucket
|
||||
isBucket,
|
||||
showLegend,
|
||||
height
|
||||
}) => {
|
||||
const { customStep, yaxis } = useGraphState();
|
||||
const { period } = useTimeState();
|
||||
|
@ -90,11 +94,11 @@ with (q = ${queryBase}) (
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-metrics-item-graph">
|
||||
<div className="vm-explore-metrics-graph">
|
||||
{isLoading && <Spinner />}
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
{warning && <Alert variant="warning">
|
||||
<div className="vm-explore-metrics-item-graph__warning">
|
||||
<div className="vm-explore-metrics-graph__warning">
|
||||
<p>{warning}</p>
|
||||
<Button
|
||||
color="warning"
|
||||
|
@ -114,6 +118,8 @@ with (q = ${queryBase}) (
|
|||
yaxis={yaxis}
|
||||
setYaxisLimits={setYaxisLimits}
|
||||
setPeriod={setPeriod}
|
||||
showLegend={showLegend}
|
||||
height={height}
|
||||
/>
|
||||
)}
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-metrics-graph {
|
||||
padding: 0 $padding-global $padding-global;
|
||||
|
||||
&__warning {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
|
||||
import ExploreMetricItemGraph from "../ExploreMetricGraph/ExploreMetricItemGraph";
|
||||
import ExploreMetricItemHeader from "../ExploreMetricItemHeader/ExploreMetricItemHeader";
|
||||
import "./style.scss";
|
||||
import useResize from "../../../hooks/useResize";
|
||||
|
||||
interface ExploreMetricItemProps {
|
||||
name: string
|
||||
job: string
|
||||
instance: string
|
||||
index: number
|
||||
onRemoveItem: (name: string) => void
|
||||
onChangeOrder: (name: string, oldIndex: number, newIndex: number) => void
|
||||
}
|
||||
|
||||
export const sizeVariants = [
|
||||
{
|
||||
id: "small",
|
||||
height: () => window.innerHeight * 0.2
|
||||
},
|
||||
{
|
||||
id: "medium",
|
||||
isDefault: true,
|
||||
height: () => window.innerHeight * 0.4
|
||||
},
|
||||
{
|
||||
id: "large",
|
||||
height: () => window.innerHeight * 0.8
|
||||
},
|
||||
];
|
||||
|
||||
const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
|
||||
name,
|
||||
job,
|
||||
instance,
|
||||
index,
|
||||
onRemoveItem,
|
||||
onChangeOrder,
|
||||
}) => {
|
||||
|
||||
const isCounter = useMemo(() => /_sum?|_total?|_count?/.test(name), [name]);
|
||||
const isBucket = useMemo(() => /_bucket?/.test(name), [name]);
|
||||
|
||||
const [rateEnabled, setRateEnabled] = useState(isCounter);
|
||||
const [showLegend, setShowLegend] = useState(false);
|
||||
const [size, setSize] = useState(sizeVariants.find(v => v.isDefault) || sizeVariants[0]);
|
||||
|
||||
const windowSize = useResize(document.body);
|
||||
const graphHeight = useMemo(size.height, [size, windowSize]);
|
||||
|
||||
const handleChangeSize = (id: string) => {
|
||||
const target = sizeVariants.find(variant => variant.id === id);
|
||||
if (target) setSize(target);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRateEnabled(isCounter);
|
||||
}, [job]);
|
||||
|
||||
return (
|
||||
<div className="vm-explore-metrics-item vm-block vm-block_empty-padding">
|
||||
<ExploreMetricItemHeader
|
||||
name={name}
|
||||
index={index}
|
||||
isBucket={isBucket}
|
||||
rateEnabled={rateEnabled}
|
||||
showLegend={showLegend}
|
||||
size={size.id}
|
||||
onChangeRate={setRateEnabled}
|
||||
onChangeLegend={setShowLegend}
|
||||
onRemoveItem={onRemoveItem}
|
||||
onChangeOrder={onChangeOrder}
|
||||
onChangeSize={handleChangeSize}
|
||||
/>
|
||||
<ExploreMetricItemGraph
|
||||
key={`${name}_${job}_${instance}_${rateEnabled}`}
|
||||
name={name}
|
||||
job={job}
|
||||
instance={instance}
|
||||
rateEnabled={rateEnabled}
|
||||
isBucket={isBucket}
|
||||
showLegend={showLegend}
|
||||
height={graphHeight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreMetricItem;
|
|
@ -0,0 +1,5 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-metrics-item {
|
||||
position: relative;
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import React, { FC, useRef, useState } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import Switch from "../../Main/Switch/Switch";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import { ArrowDownIcon, CloseIcon, ResizeIcon } from "../../Main/Icons";
|
||||
import Popper from "../../Main/Popper/Popper";
|
||||
import ExploreMetricLayouts from "../ExploreMetricLayouts/ExploreMetricLayouts";
|
||||
|
||||
interface ExploreMetricItemControlsProps {
|
||||
name: string
|
||||
index: number
|
||||
isBucket: boolean
|
||||
rateEnabled: boolean
|
||||
showLegend: boolean
|
||||
size: string
|
||||
onChangeRate: (val: boolean) => void
|
||||
onChangeLegend: (val: boolean) => void
|
||||
onRemoveItem: (name: string) => void
|
||||
onChangeOrder: (name: string, oldIndex: number, newIndex: number) => void
|
||||
onChangeSize: (id: string) => void
|
||||
}
|
||||
|
||||
const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
|
||||
name,
|
||||
index,
|
||||
isBucket,
|
||||
rateEnabled,
|
||||
showLegend,
|
||||
size,
|
||||
onChangeRate,
|
||||
onChangeLegend,
|
||||
onRemoveItem,
|
||||
onChangeOrder,
|
||||
onChangeSize
|
||||
}) => {
|
||||
|
||||
const layoutButtonRef = useRef<HTMLDivElement>(null);
|
||||
const [openPopper, setOpenPopper] = useState(false);
|
||||
const handleClickRemove = () => {
|
||||
onRemoveItem(name);
|
||||
};
|
||||
|
||||
const handleOrderDown = () => {
|
||||
onChangeOrder(name, index, index + 1);
|
||||
};
|
||||
|
||||
const handleOrderUp = () => {
|
||||
onChangeOrder(name, index, index - 1);
|
||||
};
|
||||
|
||||
const handleTogglePopper = () => {
|
||||
setOpenPopper(prev => !prev);
|
||||
};
|
||||
|
||||
const handleClosePopper = () => {
|
||||
setOpenPopper(false);
|
||||
};
|
||||
|
||||
const handleChangeSize = (id: string) => {
|
||||
onChangeSize(id);
|
||||
handleClosePopper();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-metrics-item-header">
|
||||
<div className="vm-explore-metrics-item-header-order">
|
||||
<Tooltip title="move graph up">
|
||||
<Button
|
||||
className="vm-explore-metrics-item-header-order__up"
|
||||
startIcon={<ArrowDownIcon/>}
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="small"
|
||||
onClick={handleOrderUp}
|
||||
/>
|
||||
</Tooltip>
|
||||
<div className="vm-explore-metrics-item-header__index">#{index+1}</div>
|
||||
<Tooltip title="move graph down">
|
||||
<Button
|
||||
className="vm-explore-metrics-item-header-order__down"
|
||||
startIcon={<ArrowDownIcon/>}
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="small"
|
||||
onClick={handleOrderDown}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-item-header__name">{name}</div>
|
||||
{!isBucket && (
|
||||
<Tooltip title="calculates the average per-second speed of metric's change">
|
||||
<Switch
|
||||
label={<span>enable <code>rate()</code></span>}
|
||||
value={rateEnabled}
|
||||
onChange={onChangeRate}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Switch
|
||||
label="show legend"
|
||||
value={showLegend}
|
||||
onChange={onChangeLegend}
|
||||
/>
|
||||
<div className="vm-explore-metrics-item-header__layout">
|
||||
<Tooltip title="change size the graph">
|
||||
<div ref={layoutButtonRef}>
|
||||
<Button
|
||||
startIcon={<ResizeIcon/>}
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="small"
|
||||
onClick={handleTogglePopper}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title="close graph">
|
||||
<Button
|
||||
startIcon={<CloseIcon/>}
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="small"
|
||||
onClick={handleClickRemove}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Popper
|
||||
open={openPopper}
|
||||
onClose={handleClosePopper}
|
||||
placement="bottom-right"
|
||||
buttonRef={layoutButtonRef}
|
||||
>
|
||||
<ExploreMetricLayouts
|
||||
value={size}
|
||||
onChange={handleChangeSize}
|
||||
/>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreMetricItemHeader;
|
|
@ -0,0 +1,46 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-metrics-item-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: $padding-global;
|
||||
border-bottom: $border-divider;
|
||||
gap: $padding-global;
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-order {
|
||||
display: grid;
|
||||
grid-template-columns: auto 20px auto;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: center;
|
||||
|
||||
&__up {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__layout {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: rgba($color-black, 0.05);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import React, { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { sizeVariants } from "../ExploreMetricItem/ExploreMetricItem";
|
||||
import classNames from "classnames";
|
||||
import { DoneIcon } from "../../Main/Icons";
|
||||
|
||||
interface ExploreMetricLayoutsProps {
|
||||
value: string
|
||||
onChange: (id: string) => void
|
||||
}
|
||||
|
||||
const ExploreMetricLayouts: FC<ExploreMetricLayoutsProps> = ({
|
||||
value,
|
||||
onChange
|
||||
}) => {
|
||||
|
||||
const createHandlerClick = (id: string) => () => {
|
||||
onChange(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-metrics-layouts">
|
||||
{sizeVariants.map(variant => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_multiselect": true,
|
||||
"vm-list-item_multiselect_selected": variant.id === value
|
||||
})}
|
||||
key={variant.id}
|
||||
onClick={createHandlerClick(variant.id)}
|
||||
>
|
||||
{variant.id === value && <DoneIcon/>}
|
||||
<span>{variant.id}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreMetricLayouts;
|
|
@ -0,0 +1,5 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-metrics-layouts {
|
||||
display: grid;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import React, { FC, useEffect, useMemo } from "preact/compat";
|
||||
import Select from "../../Main/Select/Select";
|
||||
import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
|
||||
import "./style.scss";
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useGraphDispatch, useGraphState } from "../../../state/graph/GraphStateContext";
|
||||
import usePrevious from "../../../hooks/usePrevious";
|
||||
|
||||
interface ExploreMetricsHeaderProps {
|
||||
jobs: string[]
|
||||
instances: string[]
|
||||
names: string[]
|
||||
job: string
|
||||
instance: string
|
||||
selectedMetrics: string[]
|
||||
onChangeJob: (job: string) => void
|
||||
onChangeInstance: (instance: string) => void
|
||||
onToggleMetric: (name: string) => void
|
||||
}
|
||||
|
||||
const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
|
||||
jobs,
|
||||
instances,
|
||||
names,
|
||||
job,
|
||||
instance,
|
||||
selectedMetrics,
|
||||
onChangeJob,
|
||||
onChangeInstance,
|
||||
onToggleMetric
|
||||
}) => {
|
||||
|
||||
const { period: { step }, duration } = useTimeState();
|
||||
const { customStep } = useGraphState();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const prevDuration = usePrevious(duration);
|
||||
|
||||
const noInstanceText = useMemo(() => job ? "" : "No instances. Please select job", [job]);
|
||||
const noMetricsText = useMemo(() => job ? "" : "No metric names. Please select job", [job]);
|
||||
|
||||
const handleChangeStep = (value: string) => {
|
||||
graphDispatch({ type: "SET_CUSTOM_STEP", payload: value });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (duration === prevDuration || !prevDuration) return;
|
||||
if (customStep) handleChangeStep(step || "1s");
|
||||
}, [duration, prevDuration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customStep && step) handleChangeStep(step);
|
||||
}, [step]);
|
||||
|
||||
return (
|
||||
<div className="vm-explore-metrics-header vm-block">
|
||||
<div className="vm-explore-metrics-header__job">
|
||||
<Select
|
||||
value={job}
|
||||
list={jobs}
|
||||
label="Job"
|
||||
placeholder="Please select job"
|
||||
onChange={onChangeJob}
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-header__instance">
|
||||
<Select
|
||||
value={instance}
|
||||
list={instances}
|
||||
label="Instance"
|
||||
placeholder="Please select instance"
|
||||
onChange={onChangeInstance}
|
||||
noOptionsText={noInstanceText}
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-header__step">
|
||||
<StepConfigurator
|
||||
defaultStep={step}
|
||||
setStep={handleChangeStep}
|
||||
value={customStep}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-header-metrics">
|
||||
<Select
|
||||
value={selectedMetrics}
|
||||
list={names}
|
||||
placeholder="Search metric name"
|
||||
onChange={onToggleMetric}
|
||||
noOptionsText={noMetricsText}
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreMetricsHeader;
|
|
@ -0,0 +1,42 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-metrics-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-small calc($padding-small + 10px);
|
||||
|
||||
&__job {
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__instance {
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__step {
|
||||
}
|
||||
|
||||
&-metrics {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../../styles/variables" as *;
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-header {
|
||||
display: flex;
|
||||
|
|
|
@ -3,6 +3,7 @@ import classNames from "classnames";
|
|||
import useClickOutside from "../../../hooks/useClickOutside";
|
||||
import Popper from "../Popper/Popper";
|
||||
import "./style.scss";
|
||||
import { DoneIcon } from "../Icons";
|
||||
|
||||
interface AutocompleteProps {
|
||||
value: string
|
||||
|
@ -13,6 +14,7 @@ interface AutocompleteProps {
|
|||
minLength?: number
|
||||
fullWidth?: boolean
|
||||
noOptionsText?: string
|
||||
selected?: string[]
|
||||
onSelect: (val: string) => void,
|
||||
onOpenAutocomplete?: (val: boolean) => void
|
||||
}
|
||||
|
@ -25,6 +27,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
|||
maxWords = 1,
|
||||
minLength = 2,
|
||||
fullWidth,
|
||||
selected,
|
||||
noOptionsText,
|
||||
onSelect,
|
||||
onOpenAutocomplete
|
||||
|
@ -56,7 +59,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
|||
const createHandlerSelect = (item: string) => () => {
|
||||
if (disabled) return;
|
||||
onSelect(item);
|
||||
handleCloseAutocomplete();
|
||||
if (!selected) handleCloseAutocomplete();
|
||||
};
|
||||
|
||||
const scrollToValue = () => {
|
||||
|
@ -84,7 +87,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
|||
if (key === "Enter") {
|
||||
const value = foundOptions[focusOption];
|
||||
value && onSelect(value);
|
||||
handleCloseAutocomplete();
|
||||
if (!selected) handleCloseAutocomplete();
|
||||
}
|
||||
|
||||
if (key === "Escape") {
|
||||
|
@ -115,7 +118,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
|||
onOpenAutocomplete && onOpenAutocomplete(openAutocomplete);
|
||||
}, [openAutocomplete]);
|
||||
|
||||
useClickOutside(wrapperEl, handleCloseAutocomplete);
|
||||
useClickOutside(wrapperEl, handleCloseAutocomplete, anchor);
|
||||
|
||||
return (
|
||||
<Popper
|
||||
|
@ -133,14 +136,17 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
|||
{foundOptions.map((option, i) =>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list__item": true,
|
||||
"vm-list__item_active": i === focusOption
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_active": i === focusOption,
|
||||
"vm-list-item_multiselect": selected,
|
||||
"vm-list-item_multiselect_selected": selected?.includes(option)
|
||||
})}
|
||||
id={`$autocomplete$${option}`}
|
||||
key={option}
|
||||
onClick={createHandlerSelect(option)}
|
||||
>
|
||||
{option}
|
||||
{selected?.includes(option) && <DoneIcon/>}
|
||||
<span>{option}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../../../styles/variables" as *;
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-calendar {
|
||||
display: grid;
|
||||
|
|
|
@ -320,3 +320,16 @@ export const SearchIcon = () => (
|
|||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ResizeIcon = () => (
|
||||
<svg
|
||||
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiBox-root css-1om0hkc"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
data-testid="OpenInFullIcon"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M21 11V3h-8l3.29 3.29-10 10L3 13v8h8l-3.29-3.29 10-10z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -22,7 +22,6 @@ const Popper: FC<PopperProps> = ({
|
|||
placement = "bottom-left",
|
||||
open = false,
|
||||
onClose,
|
||||
animation,
|
||||
offset = { top: 6, left: 0 },
|
||||
clickOutside = true,
|
||||
fullWidth
|
||||
|
@ -109,7 +108,6 @@ const Popper: FC<PopperProps> = ({
|
|||
const popperClasses = classNames({
|
||||
"vm-popper": true,
|
||||
"vm-popper_open": isOpen,
|
||||
[`vm-popper_open_${animation}`]: animation,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -13,12 +13,17 @@
|
|||
&_open {
|
||||
z-index: 101;
|
||||
opacity: 1;
|
||||
animation: scale 150ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||
transform-origin: top center;
|
||||
animation: vm-slider 150ms cubic-bezier(0.280, 0.840, 0.420, 1.1);
|
||||
pointer-events: auto;
|
||||
|
||||
&_slider {
|
||||
transform-origin: top center;
|
||||
animation: slidePopper 0.3s cubic-bezier(0.280, 0.840, 0.420, 1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vm-slider {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
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 { FormEvent, MouseEvent } from "react";
|
||||
import Autocomplete from "../Autocomplete/Autocomplete";
|
||||
import "./style.scss";
|
||||
|
||||
interface JobSelectorProps {
|
||||
value: string
|
||||
interface SelectProps {
|
||||
value: string | string[]
|
||||
list: string[]
|
||||
label?: string
|
||||
placeholder?: string
|
||||
noOptionsText?: string
|
||||
error?: string
|
||||
clearable?: boolean
|
||||
searchable?: boolean
|
||||
autofocus?: boolean
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const Select: FC<JobSelectorProps> = ({
|
||||
const Select: FC<SelectProps> = ({
|
||||
value,
|
||||
list,
|
||||
label,
|
||||
placeholder,
|
||||
error,
|
||||
noOptionsText,
|
||||
clearable = false,
|
||||
searchable,
|
||||
autofocus,
|
||||
onChange
|
||||
}) => {
|
||||
|
||||
|
@ -34,12 +31,21 @@ const Select: FC<JobSelectorProps> = ({
|
|||
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
||||
const [openList, setOpenList] = useState(false);
|
||||
|
||||
const textFieldValue = useMemo(() => openList ? search : value, [value, search, openList]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isMultiple = useMemo(() => Array.isArray(value), [value]);
|
||||
const selectedValues = useMemo(() => Array.isArray(value) ? value : undefined, [isMultiple, value]);
|
||||
|
||||
const textFieldValue = useMemo(() => {
|
||||
if (openList) return search;
|
||||
return Array.isArray(value) ? "" : value;
|
||||
}, [value, search, openList, isMultiple]);
|
||||
|
||||
const autocompleteValue = useMemo(() => !openList ? "" : search || "(.+)", [search, openList]);
|
||||
|
||||
const clearFocus = () => {
|
||||
if (document.activeElement instanceof HTMLInputElement) {
|
||||
document.activeElement.blur();
|
||||
if (inputRef.current) {
|
||||
inputRef.current.blur();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -52,65 +58,109 @@ const Select: FC<JobSelectorProps> = ({
|
|||
setOpenList(true);
|
||||
};
|
||||
|
||||
const handleClickJob = (job: string) => {
|
||||
onChange(job);
|
||||
handleCloseList();
|
||||
const handleToggleList = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target instanceof HTMLInputElement) return;
|
||||
setOpenList(prev => !prev);
|
||||
};
|
||||
|
||||
const createHandleClick = (job: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
handleClickJob(job);
|
||||
const handleSelected = (val: string) => {
|
||||
onChange(val);
|
||||
if (!isMultiple) handleCloseList();
|
||||
if (isMultiple && inputRef.current) inputRef.current.focus();
|
||||
};
|
||||
|
||||
const handleChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
setSearch((e.target as HTMLInputElement).value);
|
||||
};
|
||||
|
||||
const createHandleClick = (value: string) => (e: MouseEvent) => {
|
||||
handleSelected(value);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (inputRef.current !== e.target) {
|
||||
setOpenList(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSearch("");
|
||||
}, [openList]);
|
||||
if (openList && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
if (!openList) clearFocus();
|
||||
}, [openList, inputRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autofocus || !inputRef.current) return;
|
||||
inputRef.current.focus();
|
||||
}, [autofocus, inputRef]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="vm-select">
|
||||
<div
|
||||
className="vm-select-input"
|
||||
onClick={handleToggleList}
|
||||
ref={autocompleteAnchorEl}
|
||||
>
|
||||
<TextField
|
||||
label={label}
|
||||
type="text"
|
||||
value={textFieldValue}
|
||||
placeholder={placeholder}
|
||||
error={error}
|
||||
disabled={!searchable}
|
||||
onFocus={handleFocus}
|
||||
onEnter={handleCloseList}
|
||||
onChange={setSearch}
|
||||
endIcon={(
|
||||
<div className="vm-select-input-content">
|
||||
{selectedValues && selectedValues.map(item => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-select-input__icon": true,
|
||||
"vm-select-input__icon_open": openList
|
||||
})}
|
||||
className="vm-select-input-content__selected"
|
||||
key={item}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
{item}
|
||||
<div onClick={createHandleClick(item)}>
|
||||
<CloseIcon/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{clearable && (
|
||||
))}
|
||||
<input
|
||||
value={textFieldValue}
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
onInput={handleChange}
|
||||
onFocus={handleFocus}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</div>
|
||||
{label && <span className="vm-text-field__label">{label}</span>}
|
||||
{clearable && value && (
|
||||
<div
|
||||
className="vm-select-input__clear"
|
||||
className="vm-select-input__icon"
|
||||
onClick={createHandleClick("")}
|
||||
>
|
||||
<CloseIcon/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-select-input__icon": true,
|
||||
"vm-select-input__icon_open": openList
|
||||
})}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
</div>
|
||||
<Autocomplete
|
||||
value={autocompleteValue}
|
||||
options={list}
|
||||
anchor={autocompleteAnchorEl}
|
||||
selected={selectedValues}
|
||||
maxWords={10}
|
||||
minLength={0}
|
||||
fullWidth
|
||||
noOptionsText={noOptionsText}
|
||||
onSelect={handleClickJob}
|
||||
onSelect={handleSelected}
|
||||
onOpenAutocomplete={setOpenList}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,18 +3,82 @@
|
|||
.vm-select {
|
||||
&-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0 5px $padding-global;
|
||||
cursor: pointer;
|
||||
border: $border-divider;
|
||||
border-radius: $border-radius-small;
|
||||
min-height: 36px;
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-small;
|
||||
width: 100%;
|
||||
|
||||
&__selected {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
padding: 2px 2px 2px 6px;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: $font-size;
|
||||
line-height: $font-size;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
background-color: transparent;
|
||||
border-radius: $border-radius-small;
|
||||
transition: background-color 200ms ease-in-out;
|
||||
padding: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
pointer-events: none;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: $font-size;
|
||||
line-height: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
z-index: 2;
|
||||
min-width: 100px;
|
||||
flex-grow: 1;
|
||||
|
||||
&:placeholder-shown {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin: 0 0 0 auto;
|
||||
transition: transform 200ms ease-in;
|
||||
color: $color-text-secondary;
|
||||
border-right: $border-divider;
|
||||
transition: transform 200ms ease-in, opacity 200ms ease-in;
|
||||
cursor: pointer;
|
||||
padding: 0 $padding-small;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
|
@ -23,24 +87,6 @@
|
|||
&_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;
|
||||
|
|
|
@ -113,8 +113,8 @@ const TextField: FC<TextFieldProps> = ({
|
|||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
|
||||
/>)
|
||||
/>
|
||||
)
|
||||
}
|
||||
{label && <span className="vm-text-field__label">{label}</span>}
|
||||
<span
|
||||
|
|
|
@ -25,6 +25,7 @@ export interface GraphViewProps {
|
|||
setYaxisLimits: (val: AxisRange) => void
|
||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void
|
||||
fullWidth?: boolean
|
||||
height?: number
|
||||
}
|
||||
|
||||
const promValueToNumber = (s: string): number => {
|
||||
|
@ -53,7 +54,8 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
setYaxisLimits,
|
||||
setPeriod,
|
||||
alias = [],
|
||||
fullWidth = true
|
||||
fullWidth = true,
|
||||
height
|
||||
}) => {
|
||||
const { timezone } = useTimeState();
|
||||
const currentStep = useMemo(() => customStep || period.step || "1s", [period.step, customStep]);
|
||||
|
@ -157,6 +159,7 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
unit={unit}
|
||||
setPeriod={setPeriod}
|
||||
container={containerRef?.current}
|
||||
height={height}
|
||||
/>}
|
||||
{showLegend && <Legend
|
||||
labels={legend}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../../../styles/variables" as *;
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-table-settings-popper {
|
||||
display: grid;
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
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<ExploreMetricItemProps> = ({
|
||||
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<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRateEnabled(isCounter);
|
||||
}, [job, expanded]);
|
||||
|
||||
const Title = () => (
|
||||
<div className="vm-explore-metrics-item-header">
|
||||
<div className="vm-explore-metrics-item-header__name">{name}</div>
|
||||
{expanded && !isBucket && (
|
||||
<div
|
||||
className="vm-explore-metrics-item-header__rate"
|
||||
onClick={handleClickRate}
|
||||
>
|
||||
<Switch
|
||||
label={<span>rate()</span>}
|
||||
value={rateEnabled}
|
||||
onChange={setRateEnabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="vm-explore-metrics-item">
|
||||
<Accordion
|
||||
title={<Title/>}
|
||||
defaultExpanded={expanded}
|
||||
onChange={handleOpenAccordion}
|
||||
>
|
||||
<ExploreMetricItemGraph
|
||||
key={`${name}_${job}_${instance}_${rateEnabled}`}
|
||||
name={name}
|
||||
job={job}
|
||||
instance={instance}
|
||||
rateEnabled={rateEnabled}
|
||||
isBucket={isBucket}
|
||||
/>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreMetricItem;
|
|
@ -1,36 +0,0 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +1,25 @@
|
|||
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";
|
||||
import StepConfigurator from "../../components/Configurators/StepConfigurator/StepConfigurator";
|
||||
import { useTimeState } from "../../state/time/TimeStateContext";
|
||||
import { useGraphDispatch, useGraphState } from "../../state/graph/GraphStateContext";
|
||||
import usePrevious from "../../hooks/usePrevious";
|
||||
import ExploreMetricItem from "../../components/ExploreMetrics/ExploreMetricItem/ExploreMetricItem";
|
||||
import ExploreMetricsHeader from "../../components/ExploreMetrics/ExploreMetricsHeader/ExploreMetricsHeader";
|
||||
|
||||
const ExploreMetrics: FC = () => {
|
||||
useSetQueryParams();
|
||||
|
||||
const { period: { step }, duration } = useTimeState();
|
||||
const { customStep } = useGraphState();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const prevDuration = usePrevious(duration);
|
||||
|
||||
const [job, setJob] = useState("");
|
||||
const [instance, setInstance] = useState("");
|
||||
const [searchMetric, setSearchMetric] = useState("");
|
||||
const [openMetrics, setOpenMetrics] = useState<string[]>([]);
|
||||
const [onlyGraphs, setOnlyGraphs] = useState(false);
|
||||
const [metrics, setMetrics] = useState<string[]>([]);
|
||||
|
||||
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]);
|
||||
|
@ -56,115 +28,58 @@ const ExploreMetrics: FC = () => {
|
|||
return errorJobs || errorInstances || errorNames;
|
||||
}, [errorJobs, errorInstances, errorNames]);
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setSearchMetric("");
|
||||
const handleToggleMetric = (name: string) => {
|
||||
if (!name) {
|
||||
setMetrics([]);
|
||||
} else {
|
||||
setMetrics((prev) => prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name]);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
const handleChangeOrder = (name: string, oldIndex: number, newIndex: number) => {
|
||||
const maxIndex = newIndex > (metrics.length - 1);
|
||||
const minIndex = newIndex < 0;
|
||||
if (minIndex || maxIndex) return;
|
||||
setMetrics(prev => {
|
||||
const updatedList = [...prev];
|
||||
const [reorderedItem] = updatedList.splice(oldIndex, 1);
|
||||
updatedList.splice(newIndex, 0, reorderedItem);
|
||||
return updatedList;
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeStep = (value: string) => {
|
||||
graphDispatch({ type: "SET_CUSTOM_STEP", payload: value });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setInstance("");
|
||||
}, [job]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customStep && step) handleChangeStep(step);
|
||||
}, [step]);
|
||||
|
||||
useEffect(() => {
|
||||
if (duration === prevDuration || !prevDuration) return;
|
||||
if (customStep) handleChangeStep(step || "1s");
|
||||
}, [duration, prevDuration]);
|
||||
|
||||
return (
|
||||
<div className="vm-explore-metrics">
|
||||
<div className="vm-explore-metrics-header vm-block">
|
||||
<div className="vm-explore-metrics-header__job">
|
||||
<Select
|
||||
value={job}
|
||||
list={jobs}
|
||||
label="Job"
|
||||
placeholder="Please select job"
|
||||
onChange={setJob}
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-header__instance">
|
||||
<Select
|
||||
value={instance}
|
||||
list={instances}
|
||||
label="Instance"
|
||||
placeholder="Please select instance"
|
||||
onChange={setInstance}
|
||||
noOptionsText={noInstanceText}
|
||||
clearable
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-header__step">
|
||||
<StepConfigurator
|
||||
defaultStep={step}
|
||||
setStep={handleChangeStep}
|
||||
value={customStep}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-header__switch-graphs">
|
||||
<Switch
|
||||
label={"Show only opened metrics"}
|
||||
value={onlyGraphs}
|
||||
onChange={setOnlyGraphs}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-header__search">
|
||||
<TextField
|
||||
autofocus
|
||||
label="Metric search"
|
||||
value={searchMetric}
|
||||
onChange={setSearchMetric}
|
||||
startIcon={<SearchIcon/>}
|
||||
endIcon={(
|
||||
<div
|
||||
className="vm-explore-metrics-header__clear-icon"
|
||||
onClick={handleClearSearch}
|
||||
>
|
||||
<CloseIcon/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ExploreMetricsHeader
|
||||
jobs={jobs}
|
||||
instances={instances}
|
||||
names={names}
|
||||
job={job}
|
||||
instance={instance}
|
||||
selectedMetrics={metrics}
|
||||
onChangeJob={setJob}
|
||||
onChangeInstance={setInstance}
|
||||
onToggleMetric={handleToggleMetric}
|
||||
/>
|
||||
|
||||
{isLoading && <Spinner />}
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
{!job && <Alert variant="info">Please select job to see list of metric names.</Alert>}
|
||||
{!metrics.length && onlyGraphs && job && (
|
||||
<Alert variant="info">
|
||||
Open graphs not found. Turn off "Show only opened metrics" to see list of metric names.
|
||||
</Alert>
|
||||
)}
|
||||
{job && !metrics.length && <Alert variant="info">Please select metric names to see the graphs.</Alert>}
|
||||
<div className="vm-explore-metrics-body">
|
||||
{metrics.map((n) => (
|
||||
{metrics.map((n, i) => (
|
||||
<ExploreMetricItem
|
||||
key={n}
|
||||
name={n}
|
||||
job={job}
|
||||
instance={instance}
|
||||
openMetrics={openMetrics}
|
||||
onOpen={handleOpenMetric}
|
||||
index={i}
|
||||
onRemoveItem={handleToggleMetric}
|
||||
onChangeOrder={handleChangeOrder}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -5,67 +5,9 @@
|
|||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-small $padding-medium;
|
||||
|
||||
&__job {
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__instance {
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__step {
|
||||
}
|
||||
|
||||
&__switch-graphs {
|
||||
}
|
||||
|
||||
&__search {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
//&-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;
|
||||
// gap: $padding-medium;
|
||||
// }
|
||||
//}
|
||||
|
||||
&__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;
|
||||
gap: $padding-medium;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,35 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-list {
|
||||
&__item {
|
||||
&-item {
|
||||
padding: 12px $padding-global;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
transition: background-color 200ms ease;
|
||||
|
||||
&:hover,
|
||||
&_active {
|
||||
background-color: rgba($color-black, 0.1);
|
||||
background-color: rgba($color-black, 0.06);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.1);
|
||||
&_multiselect {
|
||||
display: grid;
|
||||
grid-template-columns: 10px 1fr;
|
||||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
svg {
|
||||
animation: vm-scale 150ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||
}
|
||||
|
||||
span {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
&_selected {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,11 @@ input {
|
|||
cursor: text;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input[type=number]::-webkit-inner-spin-button,
|
||||
input[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
|
|
|
@ -2,7 +2,6 @@ import uPlot, { Axis } from "uplot";
|
|||
import { getColorFromString } from "../color";
|
||||
|
||||
export const defaultOptions = {
|
||||
height: 500,
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue