mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-23 12:31:07 +01:00
vmui: add explore tab for exploration of metrics, which belong to a particular job/instance (#3470)
* feat: add "Explore" page * feat: add graphs for explore page * vmui: add explore tab for exploration of metrics, which belong to a particular job/instance * refactor: rename variables * refactor: extract graph to ExploreMetricItemGraph.tsx * feat: add searchable for Select.tsx * feat: improve metrics explorer * feat: set document title by page * feat: add page to view icons * fix: improve styles * fix: add encodeURIComponent to query
This commit is contained in:
parent
f6ac045933
commit
f6d31f5216
1
app/vmui/packages/vmui/.env
Normal file
1
app/vmui/packages/vmui/.env
Normal file
@ -0,0 +1 @@
|
||||
FAST_REFRESH=false
|
@ -9,7 +9,8 @@ In the project directory, you can run:
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.\
|
||||
Open [http://localhost:3000/#/icons](http://localhost:3000/#/icons) to view the icons used in the project.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
@ -10,6 +10,8 @@ import TopQueries from "./pages/TopQueries";
|
||||
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
|
||||
import Spinner from "./components/Main/Spinner/Spinner";
|
||||
import TracePage from "./pages/TracePage";
|
||||
import ExploreMetrics from "./pages/ExploreMetrics";
|
||||
import PreviewIcons from "./components/Main/Icons/PreviewIcons";
|
||||
|
||||
const App: FC = () => {
|
||||
|
||||
@ -50,6 +52,14 @@ const App: FC = () => {
|
||||
path={router.trace}
|
||||
element={<TracePage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</AppContextProvider>
|
||||
|
16
app/vmui/packages/vmui/src/api/explore-metrics.ts
Normal file
16
app/vmui/packages/vmui/src/api/explore-metrics.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TimeParams } from "../types";
|
||||
|
||||
export const getJobsUrl = (server: string, period: TimeParams): string =>
|
||||
`${server}/api/v1/label/job/values?start=${period.start}&end=${period.end}`;
|
||||
|
||||
export const getInstancesUrl = (server: string, period: TimeParams, job: string): string =>
|
||||
`${server}/api/v1/label/instance/values?match[]={job="${encodeURIComponent(job)}"}&start=${period.start}&end=${period.end}`;
|
||||
|
||||
export const getNamesUrl = (server: string, job: string, instance: string): string => {
|
||||
const match = Object.entries({ job, instance })
|
||||
.filter(val => val[1])
|
||||
.map(([key, val]) => `${key}="${val}"`)
|
||||
.join(",");
|
||||
|
||||
return `${server}/api/v1/label/__name__/values?match[]={${encodeURIComponent(match)}}`;
|
||||
};
|
@ -8,7 +8,7 @@ const Footer: FC = () => {
|
||||
|
||||
return <footer className="vm-footer">
|
||||
<a
|
||||
className="vm-footer__link vm-footer__website"
|
||||
className="vm__link vm-footer__website"
|
||||
target="_blank"
|
||||
href="https://victoriametrics.com/"
|
||||
rel="noreferrer"
|
||||
@ -17,7 +17,7 @@ const Footer: FC = () => {
|
||||
victoriametrics.com
|
||||
</a>
|
||||
<a
|
||||
className="vm-footer__link"
|
||||
className="vm__link"
|
||||
target="_blank"
|
||||
href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new"
|
||||
rel="noreferrer"
|
||||
|
@ -17,16 +17,6 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__link {
|
||||
transition: color 200ms ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__copyright {
|
||||
text-align: right;
|
||||
flex-grow: 1;
|
||||
|
@ -28,25 +28,29 @@ const Header: FC = () => {
|
||||
const { search, pathname } = useLocation();
|
||||
const routes = useMemo(() => ([
|
||||
{
|
||||
label: "Custom panel",
|
||||
label: routerOptions[router.home].title,
|
||||
value: router.home,
|
||||
},
|
||||
{
|
||||
label: "Dashboards",
|
||||
label: routerOptions[router.dashboards].title,
|
||||
value: router.dashboards,
|
||||
hide: appModeEnable
|
||||
},
|
||||
{
|
||||
label: "Cardinality",
|
||||
label: routerOptions[router.cardinality].title,
|
||||
value: router.cardinality,
|
||||
},
|
||||
{
|
||||
label: "Top queries",
|
||||
label: routerOptions[router.topQueries].title,
|
||||
value: router.topQueries,
|
||||
},
|
||||
{
|
||||
label: "Trace analyzer",
|
||||
label: routerOptions[router.trace].title,
|
||||
value: router.trace,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.metrics].title,
|
||||
value: router.metrics,
|
||||
}
|
||||
]), [appModeEnable]);
|
||||
|
||||
|
@ -1,14 +1,22 @@
|
||||
import Header from "./Header/Header";
|
||||
import React, { FC } from "preact/compat";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import React, { FC, useEffect } from "preact/compat";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import "./style.scss";
|
||||
import { getAppModeEnable } from "../../utils/app-mode";
|
||||
import classNames from "classnames";
|
||||
import Footer from "./Footer/Footer";
|
||||
import { routerOptions } from "../../router";
|
||||
|
||||
const Layout: FC = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
useEffect(() => {
|
||||
const defaultTitle = "VM UI";
|
||||
const routeTitle = routerOptions[pathname]?.title;
|
||||
document.title = routeTitle ? `${routeTitle} - ${defaultTitle}` : defaultTitle;
|
||||
}, [pathname]);
|
||||
|
||||
return <section className="vm-container">
|
||||
<Header/>
|
||||
<div
|
||||
|
@ -10,6 +10,9 @@ interface AutocompleteProps {
|
||||
anchor: Ref<HTMLElement>
|
||||
disabled?: boolean
|
||||
maxWords?: number
|
||||
minLength?: number
|
||||
fullWidth?: boolean
|
||||
noOptionsText?: string
|
||||
onSelect: (val: string) => void,
|
||||
onOpenAutocomplete?: (val: boolean) => void
|
||||
}
|
||||
@ -20,6 +23,9 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
anchor,
|
||||
disabled,
|
||||
maxWords = 1,
|
||||
minLength = 2,
|
||||
fullWidth,
|
||||
noOptionsText,
|
||||
onSelect,
|
||||
onOpenAutocomplete
|
||||
}) => {
|
||||
@ -39,6 +45,10 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
}
|
||||
}, [openAutocomplete, options, value]);
|
||||
|
||||
const displayNoOptionsText = useMemo(() => {
|
||||
return noOptionsText && !foundOptions.length;
|
||||
}, [noOptionsText,foundOptions]);
|
||||
|
||||
const handleCloseAutocomplete = () => {
|
||||
setOpenAutocomplete(false);
|
||||
};
|
||||
@ -84,7 +94,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const words = (value.match(/[a-zA-Z_:.][a-zA-Z0-9_:.]*/gm) || []).length;
|
||||
setOpenAutocomplete(value.length > 2 && words <= maxWords);
|
||||
setOpenAutocomplete(value.length > minLength && words <= maxWords);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -113,11 +123,13 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
buttonRef={anchor}
|
||||
placement="bottom-left"
|
||||
onClose={handleCloseAutocomplete}
|
||||
fullWidth={fullWidth}
|
||||
>
|
||||
<div
|
||||
className="vm-autocomplete"
|
||||
ref={wrapperEl}
|
||||
>
|
||||
{displayNoOptionsText && <div className="vm-autocomplete__no-options">{noOptionsText}</div>}
|
||||
{foundOptions.map((option, i) =>
|
||||
<div
|
||||
className={classNames({
|
||||
|
@ -3,4 +3,10 @@
|
||||
.vm-autocomplete {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
|
||||
&__no-options {
|
||||
padding: $padding-global;
|
||||
text-align: center;
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import * as icons from "./index";
|
||||
import { useSnack } from "../../../contexts/Snackbar";
|
||||
import "./style.scss";
|
||||
|
||||
const PreviewIcons: FC = () => {
|
||||
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 (
|
||||
<div className="vm-preview-icons">
|
||||
{Object.entries(icons).map(([iconKey, icon]) => (
|
||||
<div
|
||||
className="vm-preview-icons-item"
|
||||
onClick={createHandlerClickIcon(iconKey)}
|
||||
key={iconKey}
|
||||
>
|
||||
<div className="vm-preview-icons-item__svg">
|
||||
{icon()}
|
||||
</div>
|
||||
<div className="vm-preview-icons-item__name">
|
||||
{`<${iconKey}/>`}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewIcons;
|
@ -116,15 +116,6 @@ export const ArrowDownIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ArrowUpIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="m12 8-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ArrowDropDownIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
@ -318,3 +309,14 @@ export const DragIcon = () => (
|
||||
<path d="M20 9H4v2h16V9zM4 15h16v-2H4v2z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SearchIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
53
app/vmui/packages/vmui/src/components/Main/Icons/style.scss
Normal file
53
app/vmui/packages/vmui/src/components/Main/Icons/style.scss
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
@import 'src/styles/variables';
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
$padding-modal: 22px;
|
||||
|
||||
|
@ -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<PopperProps> = ({
|
||||
@ -23,7 +24,8 @@ const Popper: FC<PopperProps> = ({
|
||||
onClose,
|
||||
animation,
|
||||
offset = { top: 6, left: 0 },
|
||||
clickOutside = true
|
||||
clickOutside = true,
|
||||
fullWidth
|
||||
}) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
@ -68,7 +70,8 @@ const Popper: FC<PopperProps> = ({
|
||||
|
||||
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<PopperProps> = ({
|
||||
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);
|
||||
|
||||
|
120
app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
Normal file
120
app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
Normal file
@ -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<JobSelectorProps> = ({
|
||||
value,
|
||||
list,
|
||||
label,
|
||||
placeholder,
|
||||
error,
|
||||
noOptionsText,
|
||||
clearable = false,
|
||||
searchable,
|
||||
onChange
|
||||
}) => {
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const autocompleteAnchorEl = useRef<HTMLDivElement>(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<HTMLDivElement>) => {
|
||||
handleClickJob(job);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSearch("");
|
||||
}, [openList]);
|
||||
|
||||
return (
|
||||
<div className="vm-select">
|
||||
<div
|
||||
className="vm-select-input"
|
||||
ref={autocompleteAnchorEl}
|
||||
>
|
||||
<TextField
|
||||
label={label}
|
||||
type="text"
|
||||
value={textFieldValue}
|
||||
placeholder={placeholder}
|
||||
error={error}
|
||||
disabled={!searchable}
|
||||
onFocus={handleFocus}
|
||||
onEnter={handleCloseList}
|
||||
onChange={setSearch}
|
||||
endIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-select-input__icon": true,
|
||||
"vm-select-input__icon_open": openList
|
||||
})}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{clearable && (
|
||||
<div
|
||||
className="vm-select-input__clear"
|
||||
onClick={createHandleClick("")}
|
||||
>
|
||||
<CloseIcon/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Autocomplete
|
||||
value={autocompleteValue}
|
||||
options={list}
|
||||
anchor={autocompleteAnchorEl}
|
||||
maxWords={10}
|
||||
minLength={0}
|
||||
fullWidth
|
||||
noOptionsText={noOptionsText}
|
||||
onSelect={handleClickJob}
|
||||
onOpenAutocomplete={setOpenList}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
50
app/vmui/packages/vmui/src/components/Main/Select/style.scss
Normal file
50
app/vmui/packages/vmui/src/components/Main/Select/style.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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<TextFieldProps> = ({
|
||||
@ -23,6 +26,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||
value,
|
||||
type = "text",
|
||||
error = "",
|
||||
placeholder,
|
||||
endIcon,
|
||||
startIcon,
|
||||
disabled = false,
|
||||
@ -30,7 +34,9 @@ const TextField: FC<TextFieldProps> = ({
|
||||
helperText,
|
||||
onChange,
|
||||
onEnter,
|
||||
onKeyDown
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur
|
||||
}) => {
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -63,6 +69,14 @@ const TextField: FC<TextFieldProps> = ({
|
||||
fieldRef?.current?.focus && fieldRef.current.focus();
|
||||
}, [fieldRef, autofocus]);
|
||||
|
||||
const handleFocus = () => {
|
||||
onFocus && onFocus();
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
onBlur && onBlur();
|
||||
};
|
||||
|
||||
return <label
|
||||
className={classNames({
|
||||
"vm-text-field": true,
|
||||
@ -79,9 +93,12 @@ const TextField: FC<TextFieldProps> = ({
|
||||
disabled={disabled}
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
rows={1}
|
||||
placeholder={placeholder}
|
||||
onInput={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
@ -90,9 +107,13 @@ const TextField: FC<TextFieldProps> = ({
|
||||
disabled={disabled}
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
onInput={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
type={type}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
|
||||
/>)
|
||||
}
|
||||
{label && <span className="vm-text-field__label">{label}</span>}
|
||||
|
@ -89,7 +89,7 @@
|
||||
}
|
||||
|
||||
&_icon-start {
|
||||
padding-left: 42px;
|
||||
padding-left: 31px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { FC, useState } from "preact/compat";
|
||||
import LineProgress from "../../Main/LineProgress/LineProgress";
|
||||
import Trace from "../Trace";
|
||||
import { ArrowUpIcon } from "../../Main/Icons";
|
||||
import { ArrowDownIcon } from "../../Main/Icons";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
|
||||
@ -38,7 +38,7 @@ const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
|
||||
"vm-nested-nav-header__icon_open": openLevels[trace.idValue]
|
||||
})}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
)}
|
||||
<div className="vm-nested-nav-header__progress">
|
||||
|
@ -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<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>wrapped into <code>rate()</code></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}
|
||||
isCounter={isCounter}
|
||||
isBucket={isBucket}
|
||||
/>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreMetricItem;
|
@ -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<ExploreMetricItemGraphProps> = ({
|
||||
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 (
|
||||
<div className="vm-explore-metrics-item-graph">
|
||||
{isLoading && <Spinner />}
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
{warning && <Alert variant="warning">
|
||||
<div className="vm-explore-metrics-item-graph__warning">
|
||||
<p>{warning}</p>
|
||||
<Button
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
onClick={handleShowAll}
|
||||
>
|
||||
Show all
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>}
|
||||
{graphData && period && (
|
||||
<GraphView
|
||||
data={graphData}
|
||||
period={period}
|
||||
customStep={customStep}
|
||||
query={[query]}
|
||||
yaxis={yaxis}
|
||||
setYaxisLimits={setYaxisLimits}
|
||||
setPeriod={setPeriod}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreMetricItem;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
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 };
|
||||
};
|
@ -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<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
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 };
|
||||
};
|
@ -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<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
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 };
|
||||
};
|
@ -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, []);
|
||||
};
|
142
app/vmui/packages/vmui/src/pages/ExploreMetrics/index.tsx
Normal file
142
app/vmui/packages/vmui/src/pages/ExploreMetrics/index.tsx
Normal file
@ -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<string[]>([]);
|
||||
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 (
|
||||
<div className="vm-explore-metrics">
|
||||
<div className="vm-explore-metrics-header vm-block">
|
||||
<div className="vm-explore-metrics-header-top">
|
||||
<Select
|
||||
value={job}
|
||||
list={jobs}
|
||||
label="Job"
|
||||
placeholder="Please select job"
|
||||
onChange={setJob}
|
||||
searchable
|
||||
/>
|
||||
<Select
|
||||
value={instance}
|
||||
list={instances}
|
||||
label="Instance"
|
||||
placeholder="Please select instance"
|
||||
onChange={setInstance}
|
||||
noOptionsText={noInstanceText}
|
||||
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>
|
||||
|
||||
{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 open metrics" to see list of metric names.
|
||||
</Alert>
|
||||
)}
|
||||
<div className="vm-explore-metrics-body">
|
||||
{metrics.map((n) => (
|
||||
<ExploreMetricItem
|
||||
key={n}
|
||||
name={n}
|
||||
job={job}
|
||||
instance={instance}
|
||||
openMetrics={openMetrics}
|
||||
onOpen={handleOpenMetric}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreMetrics;
|
44
app/vmui/packages/vmui/src/pages/ExploreMetrics/style.scss
Normal file
44
app/vmui/packages/vmui/src/pages/ExploreMetrics/style.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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 = () => (
|
||||
<div className="vm-trace-page-controls">
|
||||
<Button
|
||||
@ -143,6 +148,7 @@ const TracePage: FC = () => {
|
||||
{"\n"}
|
||||
In order to use tracing please refer to the doc:
|
||||
<a
|
||||
className="vm__link vm__link_colored"
|
||||
href="https://docs.victoriametrics.com/#query-tracing"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
@ -56,7 +56,7 @@
|
||||
justify-content: center;
|
||||
|
||||
&__text {
|
||||
margin-bottom: $padding-small;
|
||||
margin-bottom: $padding-global;
|
||||
font-size: $font-size-medium;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
|
@ -3,10 +3,13 @@ const router = {
|
||||
dashboards: "/dashboards",
|
||||
cardinality: "/cardinality",
|
||||
topQueries: "/top-queries",
|
||||
trace: "/trace"
|
||||
trace: "/trace",
|
||||
metrics: "/metrics",
|
||||
icons: "/icons"
|
||||
};
|
||||
|
||||
export interface RouterOptions {
|
||||
title?: string,
|
||||
header: {
|
||||
timeSelector?: boolean,
|
||||
executionControls?: boolean,
|
||||
@ -23,12 +26,37 @@ const routerOptionsDefault = {
|
||||
};
|
||||
|
||||
export const routerOptions: {[key: string]: RouterOptions} = {
|
||||
[router.home]: routerOptionsDefault,
|
||||
[router.dashboards]: routerOptionsDefault,
|
||||
[router.home]: {
|
||||
title: "Custom panel",
|
||||
...routerOptionsDefault
|
||||
},
|
||||
[router.dashboards]: {
|
||||
title: "Dashboards",
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.cardinality]: {
|
||||
title: "Cardinality",
|
||||
header: {
|
||||
cardinalityDatePicker: true,
|
||||
}
|
||||
},
|
||||
[router.topQueries]: {
|
||||
title: "Top queries",
|
||||
header: {}
|
||||
},
|
||||
[router.trace]: {
|
||||
title: "Trace analyzer",
|
||||
header: {}
|
||||
},
|
||||
[router.metrics]: {
|
||||
title: "Explore",
|
||||
header: {
|
||||
timeSelector: true,
|
||||
}
|
||||
},
|
||||
[router.icons]: {
|
||||
title: "Icons",
|
||||
header: {}
|
||||
}
|
||||
};
|
||||
|
||||
|
15
app/vmui/packages/vmui/src/styles/components/link.scss
Normal file
15
app/vmui/packages/vmui/src/styles/components/link.scss
Normal file
@ -0,0 +1,15 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm__link {
|
||||
transition: color 200ms ease;
|
||||
cursor: pointer;
|
||||
|
||||
&_colored {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
@ -6,8 +6,9 @@
|
||||
@forward "./components/list";
|
||||
@forward "./components/popper-header";
|
||||
@forward "./components/block";
|
||||
@forward "components/sectionheader";
|
||||
@forward "./components/sectionheader";
|
||||
@forward "./components/table";
|
||||
@forward "./components/link";
|
||||
|
||||
:root {
|
||||
/* base palette */
|
||||
|
@ -63,4 +63,4 @@ $border-radius-large: 16px;
|
||||
/************* box-shadows *************/
|
||||
$box-shadow: 1px 2px 12px rgba($color-black, 0.08);
|
||||
$box-shadow-bottom: rgba($color-black, 0.04) 0px 3px 5px;
|
||||
$box-shadow-popper: rgba($color-black, 0.2) 0px 2px 8px 0px;
|
||||
$box-shadow-popper: rgba($color-black, 0.1) 0px 2px 8px 0px;
|
||||
|
@ -123,6 +123,7 @@ Released at 2022-12-11
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add ability to copy data from sources via Prometheus `remote_read` protocol. See [these docs](https://docs.victoriametrics.com/vmctl.html#migrating-data-by-remote-read-protocol). The related issues: [one](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3132) and [two](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1101).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): allow changing timezones for the requested data. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3075).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): provide fast path for hiding results for all the queries except the given one by clicking `eye` icon with `ctrl` key pressed. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3446).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add explore tab for exploration of metrics, which belong to a particular job/instance. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3386).
|
||||
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add `range_trim_spikes(phi, q)` function for trimming `phi` percent of the largest spikes per each time series returned by `q`. See [these docs](https://docs.victoriametrics.com/MetricsQL.html#range_trim_spikes).
|
||||
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): allow passing `inf` arg into [limitk](https://docs.victoriametrics.com/MetricsQL.html#limitk), [topk](https://docs.victoriametrics.com/MetricsQL.html#topk), [bottomk](https://docs.victoriametrics.com/MetricsQL.html) and other functions, which accept numeric arg, which limits the number of output time series. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3461).
|
||||
* FEATURE: [vmgateway](https://docs.victoriametrics.com/vmgateway.html): add support for JWT token signature verification. See [these docs](https://docs.victoriametrics.com/vmgateway.html#jwt-signature-verification) for details.
|
||||
|
Loading…
Reference in New Issue
Block a user