mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: fix step field (#3561)
* feat: use a unit next to the step value * app/vmselect/vmui: `make vmui-update` Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
7e49818c6d
commit
6f21435d2d
17 changed files with 181 additions and 93 deletions
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.74a50bcc.css",
|
||||
"main.js": "./static/js/main.2a5e72d0.js",
|
||||
"main.css": "./static/css/main.9a291a47.css",
|
||||
"main.js": "./static/js/main.e0294822.js",
|
||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.74a50bcc.css",
|
||||
"static/js/main.2a5e72d0.js"
|
||||
"static/css/main.9a291a47.css",
|
||||
"static/js/main.e0294822.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.2a5e72d0.js"></script><link href="./static/css/main.74a50bcc.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.e0294822.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>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.e0294822.js
Normal file
2
app/vmselect/vmui/static/js/main.e0294822.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -38,7 +38,7 @@ const AdditionalSettings: FC = () => {
|
|||
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
|
||||
};
|
||||
|
||||
const onChangeStep = (value: number) => {
|
||||
const onChangeStep = (value: string) => {
|
||||
graphDispatch({ type: "SET_CUSTOM_STEP", payload: value });
|
||||
};
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import React, { FC, useCallback, useEffect, useState } from "preact/compat";
|
||||
import debounce from "lodash.debounce";
|
||||
import React, { FC, useEffect, useState } from "preact/compat";
|
||||
import { RestartIcon } from "../../Main/Icons";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import { supportedDurations } from "../../../utils/time";
|
||||
|
||||
interface StepConfiguratorProps {
|
||||
defaultStep?: number,
|
||||
value?: number,
|
||||
setStep: (step: number) => void,
|
||||
defaultStep?: string,
|
||||
value?: string,
|
||||
setStep: (step: string) => void,
|
||||
}
|
||||
|
||||
const StepConfigurator: FC<StepConfiguratorProps> = ({ value, defaultStep, setStep }) => {
|
||||
|
@ -16,40 +17,46 @@ const StepConfigurator: FC<StepConfiguratorProps> = ({ value, defaultStep, setSt
|
|||
const [customStep, setCustomStep] = useState(value || defaultStep);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleApply = (step: number) => setStep(step || 1);
|
||||
const debouncedHandleApply = useCallback(debounce(handleApply, 500), []);
|
||||
|
||||
const onChangeStep = (val: string) => {
|
||||
const value = +val;
|
||||
if (!value) return;
|
||||
handleSetStep(value);
|
||||
const handleApply = (value?: string) => {
|
||||
const step = value || customStep || defaultStep || "1s";
|
||||
const durations = step.match(/[a-zA-Z]+/g) || [];
|
||||
setStep(!durations.length ? `${step}s` : step);
|
||||
};
|
||||
|
||||
const handleSetStep = (value: number) => {
|
||||
if (value > 0) {
|
||||
setCustomStep(value);
|
||||
debouncedHandleApply(value);
|
||||
const handleChangeStep = (value: string) => {
|
||||
const numbers = value.match(/[-+]?([0-9]*\.[0-9]+|[0-9]+)/g) || [];
|
||||
const durations = value.match(/[a-zA-Z]+/g) || [];
|
||||
const isValidNumbers = numbers.length && numbers.every(num => parseFloat(num) > 0);
|
||||
const isValidDuration = durations.every(d => supportedDurations.find(dur => dur.short === d));
|
||||
const isValidStep = isValidNumbers && isValidDuration;
|
||||
|
||||
setCustomStep(value);
|
||||
|
||||
if (isValidStep) {
|
||||
setError("");
|
||||
} else {
|
||||
setError("step is out of allowed range");
|
||||
setError(ErrorTypes.validStep);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
handleSetStep(defaultStep || 1);
|
||||
const value = defaultStep || "1s";
|
||||
handleChangeStep(value);
|
||||
handleApply(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value) handleSetStep(value);
|
||||
if (value) handleChangeStep(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="Step value of seconds"
|
||||
type="number"
|
||||
label="Step value"
|
||||
value={customStep}
|
||||
error={error}
|
||||
onChange={onChangeStep}
|
||||
onChange={handleChangeStep}
|
||||
onEnter={handleApply}
|
||||
onBlur={handleApply}
|
||||
endIcon={(
|
||||
<Tooltip title="Reset step to default">
|
||||
<Button
|
||||
|
|
|
@ -16,7 +16,7 @@ import "./style.scss";
|
|||
export interface GraphViewProps {
|
||||
data?: MetricResult[];
|
||||
period: TimeParams;
|
||||
customStep: number;
|
||||
customStep: string;
|
||||
query: string[];
|
||||
alias?: string[],
|
||||
yaxis: YaxisState;
|
||||
|
@ -56,7 +56,7 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
fullWidth = true
|
||||
}) => {
|
||||
const { timezone } = useTimeState();
|
||||
const currentStep = useMemo(() => customStep || period.step || 1, [period.step, customStep]);
|
||||
const currentStep = useMemo(() => customStep || period.step || "1s", [period.step, customStep]);
|
||||
|
||||
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
|
||||
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
||||
|
|
|
@ -15,7 +15,7 @@ interface FetchQueryParams {
|
|||
predefinedQuery?: string[]
|
||||
visible: boolean
|
||||
display?: DisplayType,
|
||||
customStep: number,
|
||||
customStep: string,
|
||||
hideQuery?: number[]
|
||||
showAllSeries?: boolean
|
||||
}
|
||||
|
|
|
@ -11,10 +11,19 @@ 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";
|
||||
|
||||
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("");
|
||||
|
@ -64,14 +73,27 @@ const ExploreMetrics: FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
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-top">
|
||||
<div className="vm-explore-metrics-header__job">
|
||||
<Select
|
||||
value={job}
|
||||
list={jobs}
|
||||
|
@ -80,6 +102,8 @@ const ExploreMetrics: FC = () => {
|
|||
onChange={setJob}
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-metrics-header__instance">
|
||||
<Select
|
||||
value={instance}
|
||||
list={instances}
|
||||
|
@ -90,29 +114,38 @@ const ExploreMetrics: FC = () => {
|
|||
clearable
|
||||
searchable
|
||||
/>
|
||||
<div className="vm-explore-metrics-header-top__switch-graphs">
|
||||
<Switch
|
||||
label={"Show only opened metrics"}
|
||||
value={onlyGraphs}
|
||||
onChange={setOnlyGraphs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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 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>
|
||||
|
||||
{isLoading && <Spinner />}
|
||||
|
|
|
@ -6,22 +6,49 @@
|
|||
gap: $padding-medium;
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
gap: $padding-small;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-small $padding-medium;
|
||||
|
||||
&-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;
|
||||
}
|
||||
&__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;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { InfoIcon } from "../../../components/Main/Icons";
|
|||
import "./style.scss";
|
||||
import Alert from "../../../components/Main/Alert/Alert";
|
||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
import usePrevious from "../../../hooks/usePrevious";
|
||||
|
||||
export interface PredefinedPanelsProps extends PanelSettings {
|
||||
filename: string;
|
||||
|
@ -27,12 +28,13 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
|
|||
alias
|
||||
}) => {
|
||||
|
||||
const { period } = useTimeState();
|
||||
const { period, duration } = useTimeState();
|
||||
const dispatch = useTimeDispatch();
|
||||
const prevDuration = usePrevious(duration);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [customStep, setCustomStep] = useState<number>(period.step || 1);
|
||||
const [customStep, setCustomStep] = useState(period.step || "1s");
|
||||
const [yaxis, setYaxis] = useState<YaxisState>({
|
||||
limits: {
|
||||
enable: false,
|
||||
|
@ -75,6 +77,11 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (duration === prevDuration || !prevDuration) return;
|
||||
if (customStep) setCustomStep(period.step || "1s");
|
||||
}, [duration, prevDuration]);
|
||||
|
||||
if (!validExpr) return (
|
||||
<Alert variant="error">
|
||||
<code>"expr"</code> not found. Check the configuration file <b>{filename}</b>.
|
||||
|
@ -119,6 +126,7 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
|
|||
<div className="vm-predefined-panel-header__step">
|
||||
<StepConfigurator
|
||||
defaultStep={period.step}
|
||||
value={customStep}
|
||||
setStep={setCustomStep}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -12,17 +12,17 @@ export interface YaxisState {
|
|||
}
|
||||
|
||||
export interface GraphState {
|
||||
customStep: number
|
||||
customStep: string
|
||||
yaxis: YaxisState
|
||||
}
|
||||
|
||||
export type GraphAction =
|
||||
| { type: "TOGGLE_ENABLE_YAXIS_LIMITS" }
|
||||
| { type: "SET_YAXIS_LIMITS", payload: AxisRange }
|
||||
| { type: "SET_CUSTOM_STEP", payload: number}
|
||||
| { type: "SET_CUSTOM_STEP", payload: string}
|
||||
|
||||
export const initialGraphState: GraphState = {
|
||||
customStep: parseFloat(getQueryStringValue("g0.step_input", "0") as string),
|
||||
customStep: getQueryStringValue("g0.step_input", "") as string,
|
||||
yaxis: {
|
||||
limits: { enable: false, range: { "1": [0, 0] } }
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export type DisplayType = "table" | "chart" | "code";
|
|||
export interface TimeParams {
|
||||
start: number; // timestamp in seconds
|
||||
end: number; // timestamp in seconds
|
||||
step?: number; // seconds
|
||||
step?: string; // seconds
|
||||
date: string; // end input date
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,8 @@ export enum ErrorTypes {
|
|||
validQuery = "Please enter a valid Query and execute it",
|
||||
traceNotFound = "Not found the tracing information",
|
||||
emptyTitle = "Please enter title",
|
||||
positiveNumber = "Please enter positive number"
|
||||
positiveNumber = "Please enter positive number",
|
||||
validStep = "Please enter a valid step"
|
||||
}
|
||||
|
||||
export interface PanelSettings {
|
||||
|
|
|
@ -11,11 +11,12 @@ export const limitsDurations = { min: 1, max: 1.578e+11 }; // min: 1 ms, max: 5
|
|||
// @ts-ignore
|
||||
export const supportedTimezones = Intl.supportedValuesOf("timeZone") as string[];
|
||||
|
||||
// The list of supported units could be the following -
|
||||
// https://prometheus.io/docs/prometheus/latest/querying/basics/#time-durations
|
||||
export const supportedDurations = [
|
||||
{ long: "days", short: "d", possible: "day" },
|
||||
{ long: "weeks", short: "w", possible: "week" },
|
||||
{ long: "months", short: "M", possible: "mon" },
|
||||
{ long: "years", short: "y", possible: "year" },
|
||||
{ long: "weeks", short: "w", possible: "week" },
|
||||
{ long: "days", short: "d", possible: "day" },
|
||||
{ long: "hours", short: "h", possible: "hour" },
|
||||
{ long: "minutes", short: "m", possible: "min" },
|
||||
{ long: "seconds", short: "s", possible: "sec" },
|
||||
|
@ -27,20 +28,24 @@ const shortDurations = supportedDurations.map(d => d.short);
|
|||
export const roundToMilliseconds = (num: number): number => Math.round(num*1000)/1000;
|
||||
|
||||
const roundStep = (step: number) => {
|
||||
let result = roundToMilliseconds(step);
|
||||
const integerStep = Math.round(step);
|
||||
|
||||
if (step >= 100) {
|
||||
return integerStep - (integerStep%10); // integer multiple of 10
|
||||
result = integerStep - (integerStep%10); // integer multiple of 10
|
||||
}
|
||||
if (step < 100 && step >= 10) {
|
||||
return integerStep - (integerStep%5); // integer multiple of 5
|
||||
result = integerStep - (integerStep%5); // integer multiple of 5
|
||||
}
|
||||
if (step < 10 && step >= 1) {
|
||||
return integerStep; // integer
|
||||
result = integerStep; // integer
|
||||
}
|
||||
if (step < 1 && step > 0.01) {
|
||||
return Math.round(step * 40) / 40; // float to thousandths multiple of 5
|
||||
result = Math.round(step * 40) / 40; // float to thousandths multiple of 5
|
||||
}
|
||||
return roundToMilliseconds(step);
|
||||
|
||||
const humanize = getDurationFromMilliseconds(dayjs.duration(result || 0.001, "seconds").asMilliseconds());
|
||||
return humanize.replace(/\s/g, "");
|
||||
};
|
||||
|
||||
export const isSupportedDuration = (str: string): Partial<Record<UnitTypeShort, string>> | undefined => {
|
||||
|
@ -53,10 +58,10 @@ export const isSupportedDuration = (str: string): Partial<Record<UnitTypeShort,
|
|||
}
|
||||
};
|
||||
|
||||
export const getTimeperiodForDuration = (dur: string, date?: Date): TimeParams => {
|
||||
const n = (date || dayjs().toDate()).valueOf() / 1000;
|
||||
|
||||
const durItems = dur.trim().split(" ");
|
||||
export const getSecondsFromDuration = (dur: string) => {
|
||||
const shortSupportedDur = supportedDurations.map(d => d.short).join("|");
|
||||
const regexp = new RegExp(`\\d+[${shortSupportedDur}]+`, "g");
|
||||
const durItems = dur.match(regexp) || [];
|
||||
|
||||
const durObject = durItems.reduce((prev, curr) => {
|
||||
|
||||
|
@ -73,9 +78,15 @@ export const getTimeperiodForDuration = (dur: string, date?: Date): TimeParams =
|
|||
}
|
||||
}, {});
|
||||
|
||||
const delta = dayjs.duration(durObject).asSeconds();
|
||||
return dayjs.duration(durObject).asSeconds();
|
||||
};
|
||||
|
||||
export const getTimeperiodForDuration = (dur: string, date?: Date): TimeParams => {
|
||||
const n = (date || dayjs().toDate()).valueOf() / 1000;
|
||||
|
||||
const delta = getSecondsFromDuration(dur);
|
||||
const rawStep = delta / MAX_ITEMS_PER_CHART;
|
||||
const step = roundStep(rawStep) || 0.001;
|
||||
const step = roundStep(rawStep);
|
||||
|
||||
return {
|
||||
start: n - delta,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import uPlot, { Axis, Series } from "uplot";
|
||||
import { getMaxFromArray, getMinFromArray } from "../math";
|
||||
import { roundToMilliseconds } from "../time";
|
||||
import { getSecondsFromDuration, roundToMilliseconds } from "../time";
|
||||
import { AxisRange } from "../../state/graph/reducer";
|
||||
import { formatTicks, sizeAxis } from "./helpers";
|
||||
import { TimeParams } from "../../types";
|
||||
|
@ -30,7 +30,8 @@ export const getAxes = (series: Series[], unit?: string): Axis[] => Array.from(n
|
|||
return axis;
|
||||
});
|
||||
|
||||
export const getTimeSeries = (times: number[], step: number, period: TimeParams): number[] => {
|
||||
export const getTimeSeries = (times: number[], stepDuration: string, period: TimeParams): number[] => {
|
||||
const step = getSecondsFromDuration(stepDuration) || 1;
|
||||
const allTimes = Array.from(new Set(times)).sort((a, b) => a - b);
|
||||
let t = period.start;
|
||||
const tEnd = roundToMilliseconds(period.end + step);
|
||||
|
|
Loading…
Reference in a new issue