vmui: add select of Tenant ID (#3673)

* feat: add select of tenantID

* feat: replace tenantID to default url

* fix: move the tenantID selector to the top header

* fix: hide tenantID selector by condition

* fix: correct z-index

* app/vmselect/vmui: `make vmui-update`

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2023-01-28 00:53:14 +01:00 committed by GitHub
parent 51ad94677c
commit ac14d50c18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 235 additions and 99 deletions

View file

@ -1,14 +1,14 @@
{
"files": {
"main.css": "./static/css/main.9ca6b743.css",
"main.js": "./static/js/main.8969be5f.js",
"main.css": "./static/css/main.9c397960.css",
"main.js": "./static/js/main.df0f4d01.js",
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
"static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.9ca6b743.css",
"static/js/main.8969be5f.js"
"static/css/main.9c397960.css",
"static/js/main.df0f4d01.js"
]
}

View file

@ -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="UI for VictoriaMetrics"/><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><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.8969be5f.js"></script><link href="./static/css/main.9ca6b743.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="UI for VictoriaMetrics"/><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><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.df0f4d01.js"></script><link href="./static/css/main.9c397960.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
export const getAccountIds = (server: string) =>
`${server.replace(/^(.+)(\/select.+)/, "$1")}/admin/tenants`;

View file

@ -1,6 +1,4 @@
import React, { FC } from "preact/compat";
import { getAppModeParams } from "../../../utils/app-mode";
import TenantsConfiguration from "../TenantsConfiguration/TenantsConfiguration";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import "./style.scss";
@ -8,8 +6,6 @@ import Switch from "../../Main/Switch/Switch";
const AdditionalSettings: FC = () => {
const { inputTenantID } = getAppModeParams();
const { autocomplete } = useQueryState();
const queryDispatch = useQueryDispatch();
@ -44,11 +40,6 @@ const AdditionalSettings: FC = () => {
value={isTracingEnabled}
onChange={onChangeQueryTracing}
/>
{!!inputTenantID && (
<div className="vm-additional-settings__input">
<TenantsConfiguration/>
</div>
)}
</div>;
};

View file

@ -1,4 +1,4 @@
import React, { FC, useState } from "preact/compat";
import React, { FC, useEffect, useState } from "preact/compat";
import ServerConfigurator from "./ServerConfigurator/ServerConfigurator";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import { SettingsIcon } from "../../Main/Icons";
@ -43,6 +43,11 @@ const GlobalSettings: FC = () => {
handleClose();
};
useEffect(() => {
if (stateServerUrl === serverUrl) return;
setServerUrl(stateServerUrl);
}, [stateServerUrl]);
return <>
<Tooltip title={title}>
<Button
@ -92,13 +97,13 @@ const GlobalSettings: FC = () => {
color="error"
onClick={handleClose}
>
Cancel
Cancel
</Button>
<Button
variant="contained"
onClick={handlerApply}
>
apply
apply
</Button>
</div>
</div>

View file

@ -22,18 +22,14 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange ,
};
return (
<div>
<div className="vm-server-configurator__title">
Server URL
</div>
<TextField
autofocus
value={serverUrl}
error={error}
onChange={onChangeServer}
onEnter={onEnter}
/>
</div>
<TextField
autofocus
label="Server URL"
value={serverUrl}
error={error}
onChange={onChangeServer}
onEnter={onEnter}
/>
);
};

View file

@ -0,0 +1,120 @@
import React, { FC, useState, useRef, useEffect, useMemo } from "preact/compat";
import { useAppDispatch, useAppState } from "../../../../state/common/StateContext";
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
import { ArrowDownIcon, StorageIcons } from "../../../Main/Icons";
import Button from "../../../Main/Button/Button";
import { useFetchAccountIds } from "./hooks/useFetchAccountIds";
import "./style.scss";
import { replaceTenantId } from "../../../../utils/default-server-url";
import classNames from "classnames";
import Popper from "../../../Main/Popper/Popper";
import { getAppModeEnable } from "../../../../utils/app-mode";
import Tooltip from "../../../Main/Tooltip/Tooltip";
const TenantsConfiguration: FC = () => {
const appModeEnable = getAppModeEnable();
const { tenantId: tenantIdState, serverUrl } = useAppState();
const dispatch = useAppDispatch();
const timeDispatch = useTimeDispatch();
const { accountIds } = useFetchAccountIds();
const [openOptions, setOpenOptions] = useState(false);
const optionsButtonRef = useRef<HTMLDivElement>(null);
const getTenantIdFromUrl = (url: string) => {
const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
return (url.match(regexp) || [])[2];
};
const showTenantSelector = useMemo(() => {
const id = getTenantIdFromUrl(serverUrl);
return accountIds.length > 1 && id;
}, [accountIds, serverUrl]);
const toggleOpenOptions = () => {
setOpenOptions(prev => !prev);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
const createHandlerChange = (value: string) => () => {
const tenant = value;
dispatch({ type: "SET_TENANT_ID", payload: tenant });
if (serverUrl) {
const updateServerUrl = replaceTenantId(serverUrl, tenant);
if (updateServerUrl === serverUrl) return;
console.log("SET_SERVER", updateServerUrl);
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
timeDispatch({ type: "RUN_QUERY" });
}
handleCloseOptions();
};
useEffect(() => {
const id = getTenantIdFromUrl(serverUrl);
if (tenantIdState && tenantIdState !== id) {
createHandlerChange(tenantIdState)();
} else {
createHandlerChange(id)();
}
}, [serverUrl]);
if (!showTenantSelector) return null;
return (
<div className="vm-tenant-input">
<Tooltip title="Define Tenant ID if you need request to another storage">
<div ref={optionsButtonRef}>
<Button
className={appModeEnable ? "" : "vm-header-button"}
variant="contained"
color="primary"
fullWidth
startIcon={<StorageIcons/>}
endIcon={(
<div
className={classNames({
"vm-execution-controls-buttons__arrow": true,
"vm-execution-controls-buttons__arrow_open": openOptions,
})}
>
<ArrowDownIcon/>
</div>
)}
onClick={toggleOpenOptions}
>
{tenantIdState}
</Button>
</div>
</Tooltip>
<Popper
open={openOptions}
placement="bottom-left"
onClose={handleCloseOptions}
buttonRef={optionsButtonRef}
fullWidth
>
<div className="vm-list">
{accountIds.map(id => (
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_active": id === tenantIdState
})}
key={id}
onClick={createHandlerChange(id)}
>
{id}
</div>
))}
</div>
</Popper>
</div>
);
};
export default TenantsConfiguration;

View file

@ -0,0 +1,41 @@
import { useAppState } from "../../../../../state/common/StateContext";
import { useEffect, useMemo, useState } from "preact/compat";
import { ErrorTypes } from "../../../../../types";
import { getAccountIds } from "../../../../../api/accountId";
export const useFetchAccountIds = () => {
const { serverUrl } = useAppState();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>();
const [accountIds, setAccountIds] = useState<string[]>([]);
const fetchUrl = useMemo(() => getAccountIds(serverUrl), [serverUrl]);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(fetchUrl);
const resp = await response.json();
const data = (resp.data || []) as string[];
setAccountIds(data);
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 { accountIds, isLoading, error };
};

View file

@ -0,0 +1,5 @@
@use "../../../../styles/variables" as *;
.vm-tenant-input {
position: relative;
}

View file

@ -8,9 +8,15 @@
&__input {
&_server {
display: grid;
grid-template-columns: 1fr auto;
gap: 0 $padding-small;
}
}
&__title {
grid-column: auto / span 2;
display: flex;
align-items: center;
justify-content: flex-start;

View file

@ -1,58 +0,0 @@
import React, { FC, useState, useEffect, useCallback } from "preact/compat";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import debounce from "lodash.debounce";
import { getAppModeParams } from "../../../utils/app-mode";
import { useTimeDispatch } from "../../../state/time/TimeStateContext";
import { InfoIcon } from "../../Main/Icons";
import TextField from "../../Main/TextField/TextField";
import Button from "../../Main/Button/Button";
import Tooltip from "../../Main/Tooltip/Tooltip";
const TenantsConfiguration: FC = () => {
const { serverURL } = getAppModeParams();
const { tenantId: tenantIdState } = useAppState();
const dispatch = useAppDispatch();
const timeDispatch = useTimeDispatch();
const [tenantId, setTenantId] = useState<string | number>(tenantIdState || 0);
const handleApply = (value: string | number) => {
const tenantId = Number(value);
dispatch({ type: "SET_TENANT_ID", payload: tenantId });
if (serverURL) {
const updateServerUrl = serverURL.replace(/(\/select\/)([\d]+)(\/prometheus)/, `$1${tenantId}$3`);
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
timeDispatch({ type: "RUN_QUERY" });
}
};
const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
const handleChange = (value: string) => {
setTenantId(value);
debouncedHandleApply(value);
};
useEffect(() => {
if (tenantId === tenantIdState) return;
setTenantId(tenantIdState);
}, [tenantIdState]);
return <TextField
label="Tenant ID"
type="number"
value={tenantId}
onChange={handleChange}
endIcon={(
<Tooltip title={"Define tenant id if you need request to another storage"}>
<Button
variant={"text"}
size={"small"}
startIcon={<InfoIcon/>}
/>
</Tooltip>
)}
/>;
};
export default TenantsConfiguration;

View file

@ -15,6 +15,7 @@ import classNames from "classnames";
import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
import { useAppState } from "../../../state/common/StateContext";
import HeaderNav from "./HeaderNav/HeaderNav";
import TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
const Header: FC = () => {
const { darkTheme } = useAppState();
@ -37,7 +38,6 @@ const Header: FC = () => {
const navigate = useNavigate();
const { search, pathname } = useLocation();
const headerSetup = useMemo(() => {
return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
}, [pathname]);
@ -70,6 +70,7 @@ const Header: FC = () => {
background={background}
/>
<div className="vm-header__settings">
{headerSetup?.tenant && <TenantsConfiguration/>}
{headerSetup?.stepControl && <StepConfigurator/>}
{headerSetup?.timeSelector && <TimeSelector/>}
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}

View file

@ -389,3 +389,14 @@ export const QuestionIcon = () => (
</svg>
);
export const StorageIcons = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M4 20h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2s.9 2 2 2zm0-3h2v2H4v-2zM2 6c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2zm4 1H4V5h2v2zm-2 7h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2s.9 2 2 2zm0-3h2v2H4v-2z"
></path>
</svg>
);

View file

@ -8,7 +8,7 @@ $padding-modal: 22px;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;

View file

@ -11,6 +11,7 @@ const router = {
export interface RouterOptions {
title?: string,
header: {
tenant?: boolean,
stepControl?: boolean,
timeSelector?: boolean,
executionControls?: boolean,
@ -21,6 +22,7 @@ export interface RouterOptions {
const routerOptionsDefault = {
header: {
tenant: true,
stepControl: true,
timeSelector: true,
executionControls: true,
@ -35,6 +37,7 @@ export const routerOptions: {[key: string]: RouterOptions} = {
[router.metrics]: {
title: "Explore metrics",
header: {
tenant: true,
stepControl: true,
timeSelector: true,
}
@ -42,12 +45,15 @@ export const routerOptions: {[key: string]: RouterOptions} = {
[router.cardinality]: {
title: "Explore cardinality",
header: {
tenant: true,
cardinalityDatePicker: true,
}
},
[router.topQueries]: {
title: "Top queries",
header: {}
header: {
tenant: true,
}
},
[router.trace]: {
title: "Trace analyzer",

View file

@ -4,18 +4,20 @@ import { getFromStorage, saveToStorage } from "../../utils/storage";
export interface AppState {
serverUrl: string;
tenantId: number;
tenantId: string;
darkTheme: boolean
}
export type Action =
| { type: "SET_SERVER", payload: string }
| { type: "SET_TENANT_ID", payload: number }
| { type: "SET_DARK_THEME", payload: boolean }
| { type: "SET_TENANT_ID", payload: string }
const tenantId = getQueryStringValue("g0.tenantID", "") as string;
export const initialState: AppState = {
serverUrl: getDefaultServer(),
tenantId: Number(getQueryStringValue("g0.tenantID", 0)),
serverUrl: getDefaultServer(tenantId),
tenantId,
darkTheme: !!getFromStorage("DARK_THEME")
};

View file

@ -1,6 +1,13 @@
import { getAppModeParams } from "./app-mode";
export const getDefaultServer = (): string => {
export const getDefaultServer = (tenantId?: string): string => {
const { serverURL } = getAppModeParams();
return serverURL || window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
const url = serverURL || window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
if (tenantId) return replaceTenantId(url, tenantId);
return url;
};
export const replaceTenantId = (serverUrl: string, tenantId: string) => {
const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
return serverUrl.replace(regexp, `$1${tenantId}/$4`);
};

View file

@ -19,6 +19,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add dark mode - it can be seleted via `settings` menu in the top right corner. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3704).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): improve visual appearance of the top menu. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3678).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): embed fonts into binary instead of loading them from external sources. This allows using `vmui` in full from isolated networks without access to Internet. Thanks to @ScottKevill for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3696).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add ability to switch between tenants by selecting the needed tenant in the drop-down list at the top right corner of the UI. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3673).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): reduce memory usage when sending stale markers for targets, which expose big number of metrics. See [this](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3668) and [this](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3675) issues.
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): allow limiting the number of concurrent requests sent to `vmauth` via `-maxConcurrentRequests` command-line flag. This allows controlling memory usage of `vmauth` and the resource usage of backends behind `vmauth`. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3346). Thanks to @dmitryk-dk for [the initial implementation](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3486).
* FEATURE: allow using VictoriaMetrics components behind proxies, which communicate with the backend via [proxy protocol](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt). See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3335). For example, [vmauth](https://docs.victoriametrics.com/vmauth.html) accepts proxy protocol connections when it starts with `-httpListenAddr.useProxyProtocol` command-line flag.