vmui: improvements to the UI styles (#3704)

* feat: add dark theme

* update packages

* feat: add multilevel menu (#3678)

* fix: correct styles

* fix: update link to cardinality-explorer

* fix: remove unused scss variables

* docs/CHANGELOG.md: document the changes

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2023-01-24 18:20:31 +01:00 committed by GitHub
parent 1a8875b417
commit 20ad848c5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1493 additions and 658 deletions

File diff suppressed because it is too large Load diff

View file

@ -17,15 +17,11 @@ const App: FC = () => {
const [loadingTheme, setLoadingTheme] = useState(true); const [loadingTheme, setLoadingTheme] = useState(true);
if (loadingTheme) return (
<>
<Spinner/>
<ThemeProvider setLoadingTheme={setLoadingTheme}/>;
</>
);
return <> return <>
<HashRouter> {loadingTheme && <Spinner/>}
<ThemeProvider setLoadingTheme={setLoadingTheme}/>
<HashRouter key={`${loadingTheme}`}>
<AppContextProvider> <AppContextProvider>
<Routes> <Routes>
<Route <Route

View file

@ -3,11 +3,13 @@ import uPlot, { Options as uPlotOptions } from "uplot";
import useResize from "../../../hooks/useResize"; import useResize from "../../../hooks/useResize";
import { BarChartProps } from "./types"; import { BarChartProps } from "./types";
import "./style.scss"; import "./style.scss";
import { useAppState } from "../../../state/common/StateContext";
const BarChart: FC<BarChartProps> = ({ const BarChart: FC<BarChartProps> = ({
data, data,
container, container,
configs }) => { configs }) => {
const { darkTheme } = useAppState();
const uPlotRef = useRef<HTMLDivElement>(null); const uPlotRef = useRef<HTMLDivElement>(null);
const [uPlotInst, setUPlotInst] = useState<uPlot>(); const [uPlotInst, setUPlotInst] = useState<uPlot>();
@ -28,7 +30,7 @@ const BarChart: FC<BarChartProps> = ({
const u = new uPlot(options, data, uPlotRef.current); const u = new uPlot(options, data, uPlotRef.current);
setUPlotInst(u); setUPlotInst(u);
return u.destroy; return u.destroy;
}, [uPlotRef.current, layoutSize]); }, [uPlotRef.current, layoutSize, darkTheme]);
useEffect(() => updateChart(), [data]); useEffect(() => updateChart(), [data]);

View file

@ -25,7 +25,6 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
pointer-events: none; pointer-events: none;
&_sticky { &_sticky {
background-color: $color-dove-gray;
pointer-events: auto; pointer-events: auto;
z-index: 99; z-index: 99;
} }

View file

@ -21,6 +21,7 @@ import "./style.scss";
import classNames from "classnames"; import classNames from "classnames";
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip"; import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useAppState } from "../../../state/common/StateContext";
export interface LineChartProps { export interface LineChartProps {
metrics: MetricResult[]; metrics: MetricResult[];
@ -47,6 +48,8 @@ const LineChart: FC<LineChartProps> = ({
container, container,
height height
}) => { }) => {
const { darkTheme } = useAppState();
const uPlotRef = useRef<HTMLDivElement>(null); const uPlotRef = useRef<HTMLDivElement>(null);
const [isPanning, setPanning] = useState(false); const [isPanning, setPanning] = useState(false);
const [xRange, setXRange] = useState({ min: period.start, max: period.end }); const [xRange, setXRange] = useState({ min: period.start, max: period.end });
@ -222,7 +225,7 @@ const LineChart: FC<LineChartProps> = ({
setUPlotInst(u); setUPlotInst(u);
setXRange({ min: period.start, max: period.end }); setXRange({ min: period.start, max: period.end });
return u.destroy; return u.destroy;
}, [uPlotRef.current, series, layoutSize, height]); }, [uPlotRef.current, series, layoutSize, height, darkTheme]);
useEffect(() => { useEffect(() => {
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);

View file

@ -13,6 +13,7 @@ import { getAppModeEnable } from "../../../utils/app-mode";
import classNames from "classnames"; import classNames from "classnames";
import Timezones from "./Timezones/Timezones"; import Timezones from "./Timezones/Timezones";
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext"; import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
import ThemeControl from "../ThemeControl/ThemeControl";
const title = "Settings"; const title = "Settings";
@ -82,6 +83,9 @@ const GlobalSettings: FC = () => {
onChange={setTimezone} onChange={setTimezone}
/> />
</div> </div>
<div className="vm-server-configurator__input">
<ThemeControl/>
</div>
<div className="vm-server-configurator__footer"> <div className="vm-server-configurator__footer">
<Button <Button
variant="outlined" variant="outlined"

View file

@ -23,7 +23,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba($color-black, 0.06); background-color: $color-hover-black;
padding: calc($padding-small/2); padding: calc($padding-small/2);
border-radius: $border-radius-small; border-radius: $border-radius-small;
} }

View file

@ -57,6 +57,7 @@ const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEn
</h3> </h3>
<Button <Button
size="small" size="small"
variant="text"
startIcon={<CloseIcon/>} startIcon={<CloseIcon/>}
onClick={handleClose} onClick={handleClose}
/> />

View file

@ -158,7 +158,7 @@ const StepConfigurator: FC = () => {
className="vm-link vm-link_colored" className="vm-link vm-link_colored"
href="https://docs.victoriametrics.com/keyConcepts.html#range-query" href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
target="_blank" target="_blank"
rel="noreferrer" rel="help noreferrer"
> >
Read more about Range query Read more about Range query
</a> </a>

View file

@ -29,7 +29,7 @@
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
margin: 0 0.2em; margin: 0 0.2em;
font-size: 85%; font-size: 85%;
background-color: rgba($color-black, 0.05); background-color: $color-hover-black;
border-radius: 6px; border-radius: 6px;
} }
} }

View file

@ -0,0 +1,44 @@
import React from "react";
import "./style.scss";
import classNames from "classnames";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
const options = [
{ title: "Light", value: false },
{ title: "Dark", value: true }
];
const ThemeControl = () => {
const { darkTheme } = useAppState();
const dispatch = useAppDispatch();
const createHandlerClickItem = (value: boolean) => () => {
dispatch({ type: "SET_DARK_THEME", payload: value });
};
return (
<div className="vm-theme-control">
<div className="vm-server-configurator__title">
Theme preferences
</div>
<div className="vm-theme-control-options">
<div
className="vm-theme-control-options__highlight"
style={{ left: darkTheme ? "50%" : 0 }}
/>
{options.map(item => (
<div
className={classNames({
"vm-theme-control-options__item": true,
"vm-theme-control-options__item_active": item.value === darkTheme
})}
onClick={createHandlerClickItem(item.value)}
key={item.title}
>{item.title}</div>
))}
</div>
</div>
);
};
export default ThemeControl;

View file

@ -0,0 +1,44 @@
@use "src/styles/variables" as *;
.vm-theme-control {
&-options {
position: relative;
display: inline-flex;
border: $border-divider;
border-radius: $border-radius-medium;
overflow: hidden;
&__item {
position: relative;
padding: $padding-small $padding-global;
border-right: $border-divider;
color: $color-text;
cursor: pointer;
z-index: 2;
transition: color 200ms ease-out;
user-select: none;
&:last-child {
border: none;
}
&_active {
color: $color-primary-text;
}
&:hover {
box-shadow: $box-shadow-popper;
}
}
&__highlight {
position: absolute;
top: 0;
left: 0;
background-color: $color-primary;
height: 100%;
width: 50%;
transition: left 150ms ease-in;
}
}
}

View file

@ -13,11 +13,14 @@ import useResize from "../../../../hooks/useResize";
import DatePicker from "../../../Main/DatePicker/DatePicker"; import DatePicker from "../../../Main/DatePicker/DatePicker";
import "./style.scss"; import "./style.scss";
import useClickOutside from "../../../../hooks/useClickOutside"; import useClickOutside from "../../../../hooks/useClickOutside";
import classNames from "classnames";
import { useAppState } from "../../../../state/common/StateContext";
export const TimeSelector: FC = () => { export const TimeSelector: FC = () => {
const { darkTheme } = useAppState();
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const documentSize = useResize(document.body); const documentSize = useResize(document.body);
const displayFullDate = useMemo(() => documentSize.width > 1120, [documentSize]); const displayFullDate = useMemo(() => documentSize.width > 1280, [documentSize]);
const [until, setUntil] = useState<string>(); const [until, setUntil] = useState<string>();
const [from, setFrom] = useState<string>(); const [from, setFrom] = useState<string>();
@ -120,7 +123,7 @@ export const TimeSelector: FC = () => {
return <> return <>
<div ref={buttonRef}> <div ref={buttonRef}>
<Tooltip title="Time range controls"> <Tooltip title={displayFullDate ? "Time range controls" : dateTitle}>
<Button <Button
className={appModeEnable ? "" : "vm-header-button"} className={appModeEnable ? "" : "vm-header-button"}
variant="contained" variant="contained"
@ -144,7 +147,12 @@ export const TimeSelector: FC = () => {
ref={wrapperRef} ref={wrapperRef}
> >
<div className="vm-time-selector-left"> <div className="vm-time-selector-left">
<div className="vm-time-selector-left-inputs"> <div
className={classNames({
"vm-time-selector-left-inputs": true,
"vm-time-selector-left-inputs_dark": darkTheme
})}
>
<div <div
className="vm-time-selector-left-inputs__date" className="vm-time-selector-left-inputs__date"
ref={fromRef} ref={fromRef}

View file

@ -18,6 +18,10 @@
align-items: flex-start; align-items: flex-start;
justify-content: stretch; justify-content: stretch;
&_dark &__date {
border-color: $color-text-disabled;
}
&__date { &__date {
display: grid; display: grid;
grid-template-columns: 1fr 14px; grid-template-columns: 1fr 14px;
@ -70,7 +74,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba($color-black, 0.06); background-color: $color-hover-black;
padding: calc($padding-small/2); padding: calc($padding-small/2);
border-radius: $border-radius-small; border-radius: $border-radius-small;
} }

View file

@ -39,7 +39,7 @@
code { code {
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
font-size: 85%; font-size: 85%;
background-color: rgba($color-black, 0.05); background-color: $color-hover-black;
border-radius: 6px; border-radius: 6px;
} }
} }

View file

@ -1,7 +1,7 @@
import React, { FC } from "preact/compat"; import React, { FC } from "preact/compat";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "./style.scss"; import "./style.scss";
import { LogoIcon } from "../../Main/Icons"; import { IssueIcon, LogoIcon, WikiIcon } from "../../Main/Icons";
const Footer: FC = () => { const Footer: FC = () => {
const copyrightYears = `2019-${dayjs().format("YYYY")}`; const copyrightYears = `2019-${dayjs().format("YYYY")}`;
@ -11,18 +11,28 @@ const Footer: FC = () => {
className="vm-link vm-footer__website" className="vm-link vm-footer__website"
target="_blank" target="_blank"
href="https://victoriametrics.com/" href="https://victoriametrics.com/"
rel="noreferrer" rel="me noreferrer"
> >
<LogoIcon/> <LogoIcon/>
victoriametrics.com victoriametrics.com
</a> </a>
<a <a
className="vm-link" className="vm-link vm-footer__link"
target="_blank"
href="https://docs.victoriametrics.com/#vmui"
rel="help noreferrer"
>
<WikiIcon/>
Documentation
</a>
<a
className="vm-link vm-footer__link"
target="_blank" target="_blank"
href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new/choose" href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new/choose"
rel="noreferrer" rel="noreferrer"
> >
create an issue <IssueIcon/>
Create an issue
</a> </a>
<div className="vm-footer__copyright"> <div className="vm-footer__copyright">
&copy; {copyrightYears} VictoriaMetrics &copy; {copyrightYears} VictoriaMetrics

View file

@ -5,10 +5,12 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: $padding-medium; padding: $padding-medium;
gap: $padding-large; gap: $padding-medium;
border-top: $border-divider; border-top: $border-divider;
color: $color-text-secondary; color: $color-text-secondary;
background: $color-background-body;
&__link,
&__website { &__website {
display: grid; display: grid;
grid-template-columns: 12px auto; grid-template-columns: 12px auto;
@ -17,6 +19,14 @@
gap: 6px; gap: 6px;
} }
&__website {
margin-right: $padding-global;
}
&__link {
grid-template-columns: 14px auto;
}
&__copyright { &__copyright {
text-align: right; text-align: right;
flex-grow: 1; flex-grow: 1;

View file

@ -1,86 +1,58 @@
import React, { FC, useMemo, useState } from "preact/compat"; import React, { FC, useMemo } from "preact/compat";
import { ExecutionControls } from "../../Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls"; import { ExecutionControls } from "../../Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
import { setQueryStringWithoutPageReload } from "../../../utils/query-string"; import { setQueryStringWithoutPageReload } from "../../../utils/query-string";
import { TimeSelector } from "../../Configurators/TimeRangeSettings/TimeSelector/TimeSelector"; import { TimeSelector } from "../../Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
import GlobalSettings from "../../Configurators/GlobalSettings/GlobalSettings"; import GlobalSettings from "../../Configurators/GlobalSettings/GlobalSettings";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import router, { RouterOptions, routerOptions } from "../../../router"; import router, { RouterOptions, routerOptions } from "../../../router";
import { useEffect } from "react";
import ShortcutKeys from "../../Main/ShortcutKeys/ShortcutKeys"; import ShortcutKeys from "../../Main/ShortcutKeys/ShortcutKeys";
import { getAppModeEnable, getAppModeParams } from "../../../utils/app-mode"; import { getAppModeEnable, getAppModeParams } from "../../../utils/app-mode";
import CardinalityDatePicker from "../../Configurators/CardinalityDatePicker/CardinalityDatePicker"; import CardinalityDatePicker from "../../Configurators/CardinalityDatePicker/CardinalityDatePicker";
import { LogoFullIcon } from "../../Main/Icons"; import { LogoFullIcon } from "../../Main/Icons";
import { getCssVariable } from "../../../utils/theme"; import { getCssVariable } from "../../../utils/theme";
import Tabs from "../../Main/Tabs/Tabs";
import "./style.scss"; import "./style.scss";
import classNames from "classnames"; import classNames from "classnames";
import { useDashboardsState } from "../../../state/dashboards/DashboardsStateContext";
import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator"; import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
import { useAppState } from "../../../state/common/StateContext";
import HeaderNav from "./HeaderNav/HeaderNav";
const Header: FC = () => { const Header: FC = () => {
const primaryColor = getCssVariable("color-primary"); const { darkTheme } = useAppState();
const appModeEnable = getAppModeEnable(); const appModeEnable = getAppModeEnable();
const { dashboardsSettings } = useDashboardsState();
const { headerStyles: { const primaryColor = useMemo(() => {
background = appModeEnable ? "#FFF" : primaryColor, const variable = darkTheme ? "color-background-block" : "color-primary";
color = appModeEnable ? primaryColor : "#FFF", return getCssVariable(variable);
} = {} } = getAppModeParams(); }, [darkTheme]);
const { background, color } = useMemo(() => {
const { headerStyles: {
background = appModeEnable ? "#FFF" : primaryColor,
color = appModeEnable ? primaryColor : "#FFF",
} = {} } = getAppModeParams();
return { background, color };
}, [primaryColor]);
const navigate = useNavigate(); const navigate = useNavigate();
const { search, pathname } = useLocation(); const { search, pathname } = useLocation();
const routes = useMemo(() => ([
{
label: routerOptions[router.home].title,
value: router.home,
},
{
label: routerOptions[router.metrics].title,
value: router.metrics,
},
{
label: routerOptions[router.cardinality].title,
value: router.cardinality,
},
{
label: routerOptions[router.topQueries].title,
value: router.topQueries,
},
{
label: routerOptions[router.trace].title,
value: router.trace,
},
{
label: routerOptions[router.dashboards].title,
value: router.dashboards,
hide: appModeEnable || !dashboardsSettings.length
}
]), [appModeEnable, dashboardsSettings]);
const [activeMenu, setActiveMenu] = useState(pathname);
const headerSetup = useMemo(() => { const headerSetup = useMemo(() => {
return ((routerOptions[pathname] || {}) as RouterOptions).header || {}; return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
}, [pathname]); }, [pathname]);
const onClickLogo = () => { const onClickLogo = () => {
navigateHandler(router.home); navigate({ pathname: router.home, search: search });
setQueryStringWithoutPageReload({}); setQueryStringWithoutPageReload({});
window.location.reload(); window.location.reload();
}; };
const navigateHandler = (pathname: string) => {
navigate({ pathname, search: search });
};
useEffect(() => {
setActiveMenu(pathname);
}, [pathname]);
return <header return <header
className={classNames({ className={classNames({
"vm-header": true, "vm-header": true,
"vm-header_app": appModeEnable "vm-header_app": appModeEnable,
"vm-header_dark": darkTheme
})} })}
style={{ background, color }} style={{ background, color }}
> >
@ -93,14 +65,10 @@ const Header: FC = () => {
<LogoFullIcon/> <LogoFullIcon/>
</div> </div>
)} )}
<div className="vm-header-nav"> <HeaderNav
<Tabs color={color}
isNavLink background={background}
activeItem={activeMenu} />
items={routes.filter(r => !r.hide)}
color={color}
/>
</div>
<div className="vm-header__settings"> <div className="vm-header__settings">
{headerSetup?.stepControl && <StepConfigurator/>} {headerSetup?.stepControl && <StepConfigurator/>}
{headerSetup?.timeSelector && <TimeSelector/>} {headerSetup?.timeSelector && <TimeSelector/>}

View file

@ -0,0 +1,89 @@
import React, { FC, useMemo, useState } from "preact/compat";
import router, { routerOptions } from "../../../../router";
import { getAppModeEnable } from "../../../../utils/app-mode";
import { useLocation } from "react-router-dom";
import { useDashboardsState } from "../../../../state/dashboards/DashboardsStateContext";
import { useEffect } from "react";
import "./style.scss";
import NavItem from "./NavItem";
import NavSubItem from "./NavSubItem";
interface HeaderNavProps {
color: string
background: string
}
const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
const appModeEnable = getAppModeEnable();
const { dashboardsSettings } = useDashboardsState();
const { pathname } = useLocation();
const [activeMenu, setActiveMenu] = useState(pathname);
const menu = useMemo(() => ([
{
label: routerOptions[router.home].title,
value: router.home,
},
{
label: "Explore",
submenu: [
{
label: routerOptions[router.metrics].title,
value: router.metrics,
},
{
label: routerOptions[router.cardinality].title,
value: router.cardinality,
},
{
label: routerOptions[router.topQueries].title,
value: router.topQueries,
},
]
},
{
label: routerOptions[router.trace].title,
value: router.trace,
},
{
label: routerOptions[router.dashboards].title,
value: router.dashboards,
hide: appModeEnable || !dashboardsSettings.length,
}
].filter(r => !r.hide)), [appModeEnable, dashboardsSettings]);
useEffect(() => {
setActiveMenu(pathname);
}, [pathname]);
return (
<nav className="vm-header-nav">
{menu.map(m => (
m.submenu
? (
<NavSubItem
key={m.label}
activeMenu={activeMenu}
label={m.label || ""}
submenu={m.submenu}
color={color}
background={background}
/>
)
: (
<NavItem
key={m.value}
activeMenu={activeMenu}
value={m.value}
label={m.label || ""}
color={color}
/>
)
))}
</nav>
);
};
export default HeaderNav;

View file

@ -0,0 +1,30 @@
import React, { FC } from "preact/compat";
import { NavLink } from "react-router-dom";
import classNames from "classnames";
interface NavItemProps {
activeMenu: string,
label: string,
value: string,
color?: string
}
const NavItem: FC<NavItemProps> = ({
activeMenu,
label,
value,
color
}) => (
<NavLink
className={classNames({
"vm-header-nav-item": true,
"vm-header-nav-item_active": activeMenu === value // || m.submenu?.find(m => m.value === activeMenu)
})}
style={{ color }}
to={value}
>
{label}
</NavLink>
);
export default NavItem;

View file

@ -0,0 +1,96 @@
import React, { FC, useRef, useState } from "preact/compat";
import { useLocation } from "react-router-dom";
import classNames from "classnames";
import { ArrowDropDownIcon } from "../../../Main/Icons";
import Popper from "../../../Main/Popper/Popper";
import NavItem from "./NavItem";
import { useEffect } from "react";
interface NavItemProps {
activeMenu: string,
label: string,
submenu: {label: string | undefined, value: string}[],
color?: string
background?: string
}
const NavSubItem: FC<NavItemProps> = ({
activeMenu,
label,
color,
background,
submenu
}) => {
const { pathname } = useLocation();
const [openSubmenu, setOpenSubmenu] = useState(false);
const [menuTimeout, setMenuTimeout] = useState<NodeJS.Timeout | null>(null);
const buttonRef = useRef<HTMLDivElement>(null);
const handleOpenSubmenu = () => {
setOpenSubmenu(true);
if (menuTimeout) clearTimeout(menuTimeout);
};
const handleCloseSubmenu = () => {
setOpenSubmenu(false);
};
const handleMouseLeave = () => {
if (menuTimeout) clearTimeout(menuTimeout);
const timeout = setTimeout(handleCloseSubmenu, 300);
setMenuTimeout(timeout);
};
const handleMouseEnterPopup = () => {
if (menuTimeout) clearTimeout(menuTimeout);
};
useEffect(() => {
handleCloseSubmenu();
}, [pathname]);
return (
<div
className={classNames({
"vm-header-nav-item": true,
"vm-header-nav-item_sub": true,
"vm-header-nav-item_open": openSubmenu,
"vm-header-nav-item_active": submenu.find(m => m.value === activeMenu)
})}
style={{ color }}
onMouseEnter={handleOpenSubmenu}
onMouseLeave={handleMouseLeave}
ref={buttonRef}
>
{label}
<ArrowDropDownIcon/>
<Popper
open={openSubmenu}
placement="bottom-left"
offset={{ top: 12, left: 0 }}
onClose={handleCloseSubmenu}
buttonRef={buttonRef}
>
<div
className="vm-header-nav-item-submenu"
style={{ background }}
onMouseLeave={handleMouseLeave}
onMouseEnter={handleMouseEnterPopup}
>
{submenu.map(sm => (
<NavItem
key={sm.value}
activeMenu={activeMenu}
value={sm.value}
label={sm.label || ""}
/>
))}
</div>
</Popper>
</div>
);
};
export default NavSubItem;

View file

@ -0,0 +1,63 @@
@use "src/styles/variables" as *;
.vm-header-nav {
display: flex;
align-items: center;
justify-content: flex-start;
gap: $padding-global;
font-size: $font-size-small;
font-weight: bold;
&-item {
position: relative;
padding: $padding-global $padding-small;
opacity: 0.5;
cursor: pointer;
transition: opacity 200ms ease-in;
text-transform: uppercase;
&_sub {
display: grid;
grid-template-columns: auto 14px;
align-items: center;
justify-content: center;
gap: 4px;
cursor: default;
}
&:hover {
opacity: 1;
}
&_active {
opacity: 1;
}
svg {
transform: rotate(0deg);
transition: transform 200ms ease-in;
}
&_open {
svg {
transform: rotate(180deg);
}
}
&-submenu {
display: grid;
white-space: nowrap;
padding: $padding-small;
color: $color-white;
border-radius: 2px;
opacity: 1;
transform-origin: top center;
font-size: $font-size-small;
font-weight: bold;
&-item {
cursor: pointer;
}
}
}
}

View file

@ -6,17 +6,18 @@
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
padding: $padding-small $padding-medium; padding: $padding-small $padding-medium;
gap: $padding-large; gap: 0 $padding-large;
z-index: 99;
&_app { &_app {
padding: $padding-small 0; padding: $padding-small 0;
} }
@media (max-width: 1200px) { &_dark {
gap: $padding-global; .vm-header-button,
button:before,
.vm-tabs { button {
gap: 0; background-color: $color-background-block;
} }
} }
@ -43,11 +44,6 @@
} }
} }
&-nav {
font-size: $font-size-small;
font-weight: 600;
}
&__settings { &__settings {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -3,6 +3,7 @@ import { ReactNode } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../Icons"; import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../Icons";
import "./style.scss"; import "./style.scss";
import { useAppState } from "../../../state/common/StateContext";
interface AlertProps { interface AlertProps {
variant?: "success" | "error" | "info" | "warning" variant?: "success" | "error" | "info" | "warning"
@ -19,12 +20,14 @@ const icons = {
const Alert: FC<AlertProps> = ({ const Alert: FC<AlertProps> = ({
variant, variant,
children }) => { children }) => {
const { darkTheme } = useAppState();
return ( return (
<div <div
className={classNames({ className={classNames({
"vm-alert": true, "vm-alert": true,
[`vm-alert_${variant}`]: variant [`vm-alert_${variant}`]: variant,
"vm-alert_dark": darkTheme
})} })}
> >
<div className="vm-alert__icon">{icons[variant || "info"]}</div> <div className="vm-alert__icon">{icons[variant || "info"]}</div>

View file

@ -11,7 +11,7 @@
border-radius: $border-radius-medium; border-radius: $border-radius-medium;
box-shadow: $box-shadow; box-shadow: $box-shadow;
font-size: $font-size-medium; font-size: $font-size-medium;
font-weight: 500; font-weight: normal;
color: $color-text; color: $color-text;
line-height: 20px; line-height: 20px;
@ -75,4 +75,14 @@
background-color: $color-warning; background-color: $color-warning;
} }
} }
&_dark {
&:after {
opacity: 0.1;
}
}
&_dark &__content {
filter: none;
}
} }

View file

@ -10,7 +10,7 @@ $button-radius: 6px;
padding: 6px 14px; padding: 6px 14px;
font-size: $font-size-small; font-size: $font-size-small;
line-height: 15px; line-height: 15px;
font-weight: 500; font-weight: normal;
min-height: 31px; min-height: 31px;
border-radius: $button-radius; border-radius: $button-radius;
color: $color-white; color: $color-white;
@ -21,7 +21,7 @@ $button-radius: 6px;
white-space: nowrap; white-space: nowrap;
&:hover:after { &:hover:after {
background-color: rgba($color-black, 0.05); background-color: $color-hover-black;
} }
&:before, &:before,

View file

@ -104,7 +104,7 @@
transition: color 200ms ease, background-color 300ms ease-in-out; transition: color 200ms ease, background-color 300ms ease-in-out;
&:hover { &:hover {
background-color: rgba($color-black, 0.05); background-color: $color-hover-black;
} }
&_empty { &_empty {
@ -144,7 +144,7 @@
transition: color 200ms ease, background-color 300ms ease-in-out; transition: color 200ms ease, background-color 300ms ease-in-out;
&:hover { &:hover {
background-color: rgba($color-black, 0.05); background-color: $color-hover-black;
} }
&_selected { &_selected {
@ -270,6 +270,10 @@
justify-content: space-between; justify-content: space-between;
margin-top: $padding-global; margin-top: $padding-global;
&_dark &__input {
border-color: $color-text-disabled;
}
span { span {
margin: 0 $padding-small; margin: 0 $padding-small;
} }
@ -277,11 +281,13 @@
&__input { &__input {
width: 64px; width: 64px;
height: 32px; height: 32px;
border: 1px solid $color-alto; border: $border-divider;
border-radius: $border-radius-small; border-radius: $border-radius-small;
font-size: $font-size-medium; font-size: $font-size-medium;
padding: 2px $padding-small; padding: 2px $padding-small;
text-align: center; text-align: center;
background-color: transparent;
color: $color-text;
&:focus { &:focus {
border-color: $color-primary; border-color: $color-primary;

View file

@ -2,6 +2,7 @@ import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import { Dayjs } from "dayjs"; import { Dayjs } from "dayjs";
import { FormEvent, FocusEvent } from "react"; import { FormEvent, FocusEvent } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { useAppState } from "../../../../state/common/StateContext";
interface CalendarTimepickerProps { interface CalendarTimepickerProps {
selectDate: Dayjs selectDate: Dayjs
@ -13,6 +14,7 @@ enum TimeUnits { hour, minutes, seconds }
const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onClose }) => { const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onClose }) => {
const { darkTheme } = useAppState();
const [activeField, setActiveField] = useState<TimeUnits>(TimeUnits.hour); const [activeField, setActiveField] = useState<TimeUnits>(TimeUnits.hour);
const [hours, setHours] = useState(selectDate.format("HH")); const [hours, setHours] = useState(selectDate.format("HH"));
@ -154,7 +156,12 @@ const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onCl
</div> </div>
))} ))}
</div> </div>
<div className="vm-calendar-time-picker-fields"> <div
className={classNames({
"vm-calendar-time-picker-fields": true,
"vm-calendar-time-picker-fields_dark": darkTheme
})}
>
<input <input
className="vm-calendar-time-picker-fields__input" className="vm-calendar-time-picker-fields__input"
value={hours} value={hours}

View file

@ -344,3 +344,48 @@ export const TimelineIcon = () => (
></path> ></path>
</svg> </svg>
); );
export const WikiIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21 5C19.89 4.65 18.67 4.5 17.5 4.5C15.55 4.5 13.45 4.9 12 6C10.55 4.9 8.45 4.5 6.5 4.5C5.33 4.5 4.11 4.65 3 5C2.25 5.25 1.6 5.55 1 6V20.6C1 20.85 1.25 21.1 1.5 21.1C1.6 21.1 1.65 21.1 1.75 21.05C3.15 20.3 4.85 20 6.5 20C8.2 20 10.65 20.65 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5ZM21 18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5C10.65 18.65 8.2 18 6.5 18C5.3 18 4.1 18.15 3 18.5V7C4.1 6.65 5.3 6.5 6.5 6.5C8.2 6.5 10.65 7.15 12 8C13.35 7.15 15.8 6.5 17.5 6.5C18.7 6.5 19.9 6.65 21 7V18.5Z"
/>
<path
d="M17.5 10.5C18.38 10.5 19.23 10.59 20 10.76V9.24C19.21 9.09 18.36 9 17.5 9C15.8 9 14.26 9.29 13 9.83V11.49C14.13 10.85 15.7 10.5 17.5 10.5ZM13 12.49V14.15C14.13 13.51 15.7 13.16 17.5 13.16C18.38 13.16 19.23 13.25 20 13.42V11.9C19.21 11.75 18.36 11.66 17.5 11.66C15.8 11.66 14.26 11.96 13 12.49ZM17.5 14.33C15.8 14.33 14.26 14.62 13 15.16V16.82C14.13 16.18 15.7 15.83 17.5 15.83C18.38 15.83 19.23 15.92 20 16.09V14.57C19.21 14.41 18.36 14.33 17.5 14.33Z"
/>
<path
d="M6.5 10.5C5.62 10.5 4.77 10.59 4 10.76V9.24C4.79 9.09 5.64 9 6.5 9C8.2 9 9.74 9.29 11 9.83V11.49C9.87 10.85 8.3 10.5 6.5 10.5ZM11 12.49V14.15C9.87 13.51 8.3 13.16 6.5 13.16C5.62 13.16 4.77 13.25 4 13.42V11.9C4.79 11.75 5.64 11.66 6.5 11.66C8.2 11.66 9.74 11.96 11 12.49ZM6.5 14.33C8.2 14.33 9.74 14.62 11 15.16V16.82C9.87 16.18 8.3 15.83 6.5 15.83C5.62 15.83 4.77 15.92 4 16.09V14.57C4.79 14.41 5.64 14.33 6.5 14.33Z"
/>
</svg>
);
export const IssueIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10 10-4.49 10-10S17.51 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3-8c0 1.66-1.34 3-3 3s-3-1.34-3-3 1.34-3 3-3 3 1.34 3 3z"
></path>
</svg>
);
export const QuestionIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 6C9.79 6 8 7.79 8 10H10C10 8.9 10.9 8 12 8C13.1 8 14 8.9 14 10C14 10.8792 13.4202 11.3236 12.7704 11.8217C11.9421 12.4566 11 13.1787 11 15H13C13 13.9046 13.711 13.2833 14.4408 12.6455C15.21 11.9733 16 11.2829 16 10C16 7.79 14.21 6 12 6ZM13 16V18H11V16H13Z"
/>
</svg>
);

View file

@ -11,7 +11,7 @@
&-track { &-track {
width: 100%; width: 100%;
height: 20px; height: 20px;
background-color: rgba($color-black, 0.05); background-color: $color-hover-black;
border-radius: $border-radius-small; border-radius: $border-radius-small;
&__thumb { &__thumb {

View file

@ -8,7 +8,7 @@ $padding-modal: 22px;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 100; z-index: 999;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -16,9 +16,11 @@ $padding-modal: 22px;
&-content { &-content {
padding: $padding-modal; padding: $padding-modal;
background: $color-white; background: $color-background-block;
box-shadow: 0 0 24px rgba($color-black, 0.07); box-shadow: 0 0 24px rgba($color-black, 0.07);
border-radius: $border-radius-small; border-radius: $border-radius-small;
max-height: 90vh;
overflow: auto;
&-header { &-header {
display: grid; display: grid;

View file

@ -3,6 +3,7 @@ import classNames from "classnames";
import { ArrowDropDownIcon, CloseIcon } from "../Icons"; import { ArrowDropDownIcon, CloseIcon } from "../Icons";
import { FormEvent, MouseEvent } from "react"; import { FormEvent, MouseEvent } from "react";
import Autocomplete from "../Autocomplete/Autocomplete"; import Autocomplete from "../Autocomplete/Autocomplete";
import { useAppState } from "../../../state/common/StateContext";
import "./style.scss"; import "./style.scss";
interface SelectProps { interface SelectProps {
@ -26,6 +27,7 @@ const Select: FC<SelectProps> = ({
autofocus, autofocus,
onChange onChange
}) => { }) => {
const { darkTheme } = useAppState();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const autocompleteAnchorEl = useRef<HTMLDivElement>(null); const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
@ -106,7 +108,12 @@ const Select: FC<SelectProps> = ({
}, []); }, []);
return ( return (
<div className="vm-select"> <div
className={classNames({
"vm-select": true,
"vm-select_dark": darkTheme
})}
>
<div <div
className="vm-select-input" className="vm-select-input"
onClick={handleToggleList} onClick={handleToggleList}

View file

@ -24,7 +24,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba($color-black, 0.06); background-color: $color-hover-black;
padding: 2px 2px 2px 6px; padding: 2px 2px 2px 6px;
border-radius: $border-radius-small; border-radius: $border-radius-small;
font-size: $font-size; font-size: $font-size;
@ -60,6 +60,8 @@
z-index: 2; z-index: 2;
min-width: 100px; min-width: 100px;
flex-grow: 1; flex-grow: 1;
background-color: transparent;
color: $color-text;
&:placeholder-shown { &:placeholder-shown {
width: auto; width: auto;

View file

@ -35,7 +35,7 @@
line-height: 2; line-height: 2;
color: $color-text; color: $color-text;
text-align: center; text-align: center;
background-color: $color-white; background-color: $color-background-body;
background-repeat: repeat-x; background-repeat: repeat-x;
border: $border-divider; border: $border-divider;
border-radius: 4px; border-radius: 4px;

View file

@ -1,5 +1,7 @@
import React, { CSSProperties, FC } from "preact/compat"; import React, { CSSProperties, FC } from "preact/compat";
import "./style.scss"; import "./style.scss";
import classNames from "classnames";
import { getFromStorage } from "../../../utils/storage";
interface SpinnerProps { interface SpinnerProps {
containerStyles?: CSSProperties; containerStyles?: CSSProperties;
@ -8,7 +10,10 @@ interface SpinnerProps {
const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => ( const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => (
<div <div
className="vm-spinner" className={classNames({
"vm-spinner": true,
"vm-spinner_dark": getFromStorage("DARK_THEME")
})}
style={containerStyles && {}} style={containerStyles && {}}
> >
<div className="half-circle-spinner"> <div className="half-circle-spinner">

View file

@ -15,6 +15,10 @@
z-index: 99; z-index: 99;
animation: vm-fade 2s cubic-bezier(0.280, 0.840, 0.420, 1.1); animation: vm-fade 2s cubic-bezier(0.280, 0.840, 0.420, 1.1);
&_dark {
background-color: rgba($color-black, 0.2);
}
&__message { &__message {
margin-top: $padding-medium; margin-top: $padding-medium;
white-space: pre-line; white-space: pre-line;

View file

@ -1,6 +1,7 @@
import React, { FC, KeyboardEvent, useEffect, useRef, HTMLInputTypeAttribute, ReactNode } from "react"; import React, { FC, KeyboardEvent, useEffect, useRef, HTMLInputTypeAttribute, ReactNode } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { useMemo } from "preact/compat"; import { useMemo } from "preact/compat";
import { useAppState } from "../../../state/common/StateContext";
import "./style.scss"; import "./style.scss";
interface TextFieldProps { interface TextFieldProps {
@ -38,6 +39,7 @@ const TextField: FC<TextFieldProps> = ({
onFocus, onFocus,
onBlur onBlur
}) => { }) => {
const { darkTheme } = useAppState();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -81,6 +83,7 @@ const TextField: FC<TextFieldProps> = ({
className={classNames({ className={classNames({
"vm-text-field": true, "vm-text-field": true,
"vm-text-field_textarea": type === "textarea", "vm-text-field_textarea": type === "textarea",
"vm-text-field_dark": darkTheme
})} })}
data-replicated-value={value} data-replicated-value={value}
> >

View file

@ -67,6 +67,8 @@
min-height: 34px; min-height: 34px;
resize: none; resize: none;
overflow: hidden; overflow: hidden;
background-color: transparent;
color: $color-text;
&:focus { &:focus {
border: 1px solid $color-primary; border: 1px solid $color-primary;

View file

@ -2,6 +2,8 @@ import { FC, useEffect } from "preact/compat";
import { getContrastColor } from "../../../utils/color"; import { getContrastColor } from "../../../utils/color";
import { getCssVariable, setCssVariable } from "../../../utils/theme"; import { getCssVariable, setCssVariable } from "../../../utils/theme";
import { AppParams, getAppModeParams } from "../../../utils/app-mode"; import { AppParams, getAppModeParams } from "../../../utils/app-mode";
import { getFromStorage } from "../../../utils/storage";
import { darkPalette, lightPalette } from "../../../constants/palette";
interface StyleVariablesProps { interface StyleVariablesProps {
setLoadingTheme: (val: boolean) => void setLoadingTheme: (val: boolean) => void
@ -27,13 +29,6 @@ export const ThemeProvider: FC<StyleVariablesProps> = ({ setLoadingTheme }) => {
setCssVariable("scrollbar-height", `${innerHeight - clientHeight}px`); setCssVariable("scrollbar-height", `${innerHeight - clientHeight}px`);
}; };
const setAppModePalette = () => {
colorVariables.forEach(variable => {
const colorFromAppMode = palette[variable as keyof AppParams["palette"]];
if (colorFromAppMode) setCssVariable(`color-${variable}`, colorFromAppMode);
});
};
const setContrastText = () => { const setContrastText = () => {
colorVariables.forEach(variable => { colorVariables.forEach(variable => {
const color = getCssVariable(`color-${variable}`); const color = getCssVariable(`color-${variable}`);
@ -42,11 +37,34 @@ export const ThemeProvider: FC<StyleVariablesProps> = ({ setLoadingTheme }) => {
}); });
}; };
const setAppModePalette = () => {
colorVariables.forEach(variable => {
const colorFromAppMode = palette[variable as keyof AppParams["palette"]];
if (colorFromAppMode) setCssVariable(`color-${variable}`, colorFromAppMode);
});
setContrastText();
};
const setTheme = () => {
const darkTheme = getFromStorage("DARK_THEME");
const palette = darkTheme ? darkPalette : lightPalette;
Object.entries(palette).forEach(([variable, value]) => {
setCssVariable(variable, value);
});
setContrastText();
};
useEffect(() => { useEffect(() => {
setAppModePalette(); setAppModePalette();
setScrollbarSize(); setScrollbarSize();
setContrastText(); setTheme();
setLoadingTheme(false); setLoadingTheme(false);
window.addEventListener("storage", setTheme);
return () => {
window.removeEventListener("storage", setTheme);
};
}, []); }, []);
return null; return null;

View file

@ -4,6 +4,7 @@ import Trace from "../Trace";
import { ArrowDownIcon } from "../../Main/Icons"; import { ArrowDownIcon } from "../../Main/Icons";
import "./style.scss"; import "./style.scss";
import classNames from "classnames"; import classNames from "classnames";
import { useAppState } from "../../../state/common/StateContext";
interface RecursiveProps { interface RecursiveProps {
trace: Trace; trace: Trace;
@ -15,6 +16,7 @@ interface OpenLevels {
} }
const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => { const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
const { darkTheme } = useAppState();
const [openLevels, setOpenLevels] = useState({} as OpenLevels); const [openLevels, setOpenLevels] = useState({} as OpenLevels);
const handleListClick = (level: number) => () => { const handleListClick = (level: number) => () => {
@ -26,7 +28,12 @@ const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
const progress = trace.duration / totalMsec * 100; const progress = trace.duration / totalMsec * 100;
return ( return (
<div className="vm-nested-nav"> <div
className={classNames({
"vm-nested-nav": true,
"vm-nested-nav_dark": darkTheme,
})}
>
<div <div
className="vm-nested-nav-header" className="vm-nested-nav-header"
onClick={handleListClick(trace.idValue)} onClick={handleListClick(trace.idValue)}

View file

@ -5,6 +5,10 @@
border-radius: $border-radius-small; border-radius: $border-radius-small;
background-color: rgba($color-tropical-blue, 0.4); background-color: rgba($color-tropical-blue, 0.4);
&_dark {
background-color: rgba($color-black, 0.1);
}
&-header { &-header {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
@ -15,7 +19,7 @@
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: rgba($color-black, 0.06); background-color: $color-hover-black;
} }
&__icon { &__icon {

View file

@ -14,5 +14,6 @@
transform: translateY(-32px); transform: translateY(-32px);
font-size: $font-size; font-size: $font-size;
line-height: 1.4; line-height: 1.4;
white-space: pre-wrap;
} }
} }

View file

@ -0,0 +1,37 @@
export const darkPalette = {
"color-primary": "#589DF6",
"color-secondary": "#316eca",
"color-error": "#e5534b",
"color-warning": "#c69026",
"color-info": "#539bf5",
"color-success": "#57ab5a",
"color-background-body": "#22272e",
"color-background-block": "#2d333b",
"color-background-tooltip": "rgba(22, 22, 22, 0.6)",
"color-text": "#cdd9e5",
"color-text-secondary": "#768390",
"color-text-disabled": "#636e7b",
"box-shadow": "rgba(0, 0, 0, 0.16) 1px 2px 6px",
"box-shadow-popper": "rgba(0, 0, 0, 0.2) 0px 2px 8px 0px",
"border-divider": "1px solid rgba(99, 110, 123, 0.5)",
"color-hover-black": "rgba(0, 0, 0, 0.12)"
};
export const lightPalette = {
"color-primary": "#3F51B5",
"color-secondary": "#E91E63",
"color-error": "#FD080E",
"color-warning": "#FF8308",
"color-info": "#03A9F4",
"color-success": "#4CAF50",
"color-background-body": "#FEFEFF",
"color-background-block": "#FFFFFF",
"color-background-tooltip": "rgba(97,97,97, 0.92)",
"color-text": "#110f0f",
"color-text-secondary": "#706F6F",
"color-text-disabled": "#A09F9F",
"box-shadow": "rgba(0, 0, 0, 0.08) 1px 2px 6px",
"box-shadow-popper": "rgba(0, 0, 0, 0.1) 0px 2px 8px 0px",
"border-divider": "1px solid rgba(0, 0, 0, 0.15)",
"color-hover-black": "rgba(0, 0, 0, 0.06)"
};

View file

@ -4,11 +4,12 @@ import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
import { ErrorTypes } from "../../../types"; import { ErrorTypes } from "../../../types";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext"; import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import Switch from "../../../components/Main/Switch/Switch"; import Switch from "../../../components/Main/Switch/Switch";
import { PlayIcon } from "../../../components/Main/Icons"; import { PlayIcon, QuestionIcon } from "../../../components/Main/Icons";
import Button from "../../../components/Main/Button/Button"; import Button from "../../../components/Main/Button/Button";
import TextField from "../../../components/Main/TextField/TextField"; import TextField from "../../../components/Main/TextField/TextField";
import "./style.scss"; import "./style.scss";
import { useMemo } from "preact/compat"; import { useMemo } from "preact/compat";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
export interface CardinalityConfiguratorProps { export interface CardinalityConfiguratorProps {
onSetHistory: (step: number) => void; onSetHistory: (step: number) => void;
@ -92,13 +93,13 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
onChange={onFocusLabelChange} onChange={onFocusLabelChange}
/> />
</div> </div>
<div className="vm-cardinality-configurator-controls__item"> </div>
<Switch <div className="vm-cardinality-configurator-bottom__autocomplete">
label={"Autocomplete"} <Switch
value={autocomplete} label={"Autocomplete"}
onChange={onChangeAutocomplete} value={autocomplete}
/> onChange={onChangeAutocomplete}
</div> />
</div> </div>
<div className="vm-cardinality-configurator-bottom"> <div className="vm-cardinality-configurator-bottom">
<div className="vm-cardinality-configurator-bottom__info"> <div className="vm-cardinality-configurator-bottom__info">
@ -106,6 +107,19 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}. at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}.
Show top {topN} entries per table. Show top {topN} entries per table.
</div> </div>
<a
className="vm-cardinality-configurator-bottom__docs"
href="https://victoriametrics.com/blog/cardinality-explorer/"
target="_blank"
rel="help noreferrer"
>
<Tooltip title="Example of using">
<Button
variant="text"
startIcon={<QuestionIcon/>}
/>
</Tooltip>
</a>
<Button <Button
startIcon={<PlayIcon/>} startIcon={<PlayIcon/>}
onClick={onRunQuery} onClick={onRunQuery}

View file

@ -18,12 +18,18 @@
&-bottom { &-bottom {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto auto;
align-items: flex-end; align-items: flex-end;
gap: $padding-medium; gap: $padding-small;
&__info { &__info {
font-size: $font-size; font-size: $font-size;
} }
&__docs {
display: flex;
align-items: center;
justify-content: center;
}
} }
} }

View file

@ -89,6 +89,7 @@ const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns = [], onCh
onClick={handleClose} onClick={handleClose}
startIcon={<CloseIcon/>} startIcon={<CloseIcon/>}
size="small" size="small"
variant="text"
/> />
</div> </div>
<div className="vm-table-settings-popper-list"> <div className="vm-table-settings-popper-list">

View file

@ -2,20 +2,25 @@
.vm-json-form { .vm-json-form {
display: grid; display: grid;
grid-template-rows: auto calc(90vh - 78px - ($padding-medium*3)) auto; grid-template-rows: auto calc(70vh - 78px - ($padding-medium*3)) auto;
gap: $padding-global; gap: $padding-global;
width: 70vw; width: 70vw;
max-width: 1000px; max-width: 1000px;
max-height: 900px; max-height: 900px;
overflow: hidden;
&_one-field { &_one-field {
grid-template-rows: calc(90vh - 78px - ($padding-medium*3)) auto; grid-template-rows: calc(70vh - 78px - ($padding-medium*3)) auto;
}
.vm-text-field_textarea {
} }
textarea { textarea {
overflow: auto; overflow: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 900px;
} }
&-footer { &-footer {

View file

@ -151,7 +151,7 @@ const TracePage: FC = () => {
className="vm-link vm-link_colored" className="vm-link vm-link_colored"
href="https://docs.victoriametrics.com/#query-tracing" href="https://docs.victoriametrics.com/#query-tracing"
target="_blank" target="_blank"
rel="noreferrer" rel="help noreferrer"
> >
https://docs.victoriametrics.com/#query-tracing https://docs.victoriametrics.com/#query-tracing
</a> </a>

View file

@ -1,18 +1,22 @@
import { getDefaultServer } from "../../utils/default-server-url"; import { getDefaultServer } from "../../utils/default-server-url";
import { getQueryStringValue } from "../../utils/query-string"; import { getQueryStringValue } from "../../utils/query-string";
import { getFromStorage, saveToStorage } from "../../utils/storage";
export interface AppState { export interface AppState {
serverUrl: string; serverUrl: string;
tenantId: number; tenantId: number;
darkTheme: boolean
} }
export type Action = export type Action =
| { type: "SET_SERVER", payload: string } | { type: "SET_SERVER", payload: string }
| { type: "SET_TENANT_ID", payload: number } | { type: "SET_TENANT_ID", payload: number }
| { type: "SET_DARK_THEME", payload: boolean }
export const initialState: AppState = { export const initialState: AppState = {
serverUrl: getDefaultServer(), serverUrl: getDefaultServer(),
tenantId: Number(getQueryStringValue("g0.tenantID", 0)), tenantId: Number(getQueryStringValue("g0.tenantID", 0)),
darkTheme: !!getFromStorage("DARK_THEME")
}; };
export function reducer(state: AppState, action: Action): AppState { export function reducer(state: AppState, action: Action): AppState {
@ -27,6 +31,12 @@ export function reducer(state: AppState, action: Action): AppState {
...state, ...state,
tenantId: action.payload tenantId: action.payload
}; };
case "SET_DARK_THEME":
saveToStorage("DARK_THEME", action.payload);
return {
...state,
darkTheme: action.payload
};
default: default:
throw new Error(); throw new Error();
} }

View file

@ -9,7 +9,7 @@
&:hover, &:hover,
&_active { &_active {
background-color: rgba($color-black, 0.06); background-color: $color-hover-black;
} }
&_multiselect { &_multiselect {

View file

@ -6,10 +6,11 @@
gap: $padding-small; gap: $padding-small;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: $color-primary; background-color: $color-background-block;
padding: $padding-small $padding-small $padding-small $padding-global; padding: $padding-small $padding-small $padding-small $padding-global;
border-radius: $border-radius-small $border-radius-small 0 0; border-radius: $border-radius-small $border-radius-small 0 0;
color: $color-white; color: $color-text;
border-bottom: $border-divider;
&__title { &__title {
font-weight: bold; font-weight: bold;

View file

@ -12,7 +12,7 @@
transition: background-color 200ms ease; transition: background-color 200ms ease;
&:hover:not(&_header) { &:hover:not(&_header) {
background-color: rgba($color-black, 0.05); background-color: $color-hover-black;
} }
&_header { &_header {
@ -43,7 +43,7 @@
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: rgba($color-black, 0.05); background-color: $color-hover-black;
} }
} }
@ -54,7 +54,8 @@
} }
&_gray { &_gray {
color: rgba($color-black, 0.4); color: $color-text;
opacity: 0.4;
} }
&_right { &_right {

View file

@ -9,6 +9,7 @@ html, body, #root {
background-repeat: no-repeat; background-repeat: no-repeat;
background-attachment: fixed; background-attachment: fixed;
margin: 0; margin: 0;
background-color: $color-background-body;
} }
body { body {
@ -54,3 +55,24 @@ input[type=number]::-webkit-outer-spin-button {
svg { svg {
width: 100%; width: 100%;
} }
/* Works on Firefox */
* {
scrollbar-width: thin;
scrollbar-color: $color-text-disabled $color-background-block;
}
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 12px;
}
*::-webkit-scrollbar-track {
background: $color-background-block;
}
*::-webkit-scrollbar-thumb {
background-color: $color-text-disabled;
border-radius: 20px;
border: 3px solid $color-background-block;
}

View file

@ -30,4 +30,17 @@
/* backgrounds */ /* backgrounds */
--color-background-body: #FEFEFF; --color-background-body: #FEFEFF;
--color-background-block: #FFFFFF; --color-background-block: #FFFFFF;
--color-background-tooltip: rgba(97,97,97, 0.92);
/* text */
--color-text: #110f0f;
--color-text-secondary: #706F6F;
--color-text-disabled: #A09F9F;
/* box-shadow */
--box-shadow: rgba(0, 0, 0, 0.08) 1px 2px 6px;
--box-shadow-popper: rgba(0, 0, 0, 0.1) 0px 2px 8px 0px;
--border-divider: 1px solid rgba(0, 0, 0, 0.15);
--color-hover-black: rgba(0, 0, 0, 0.06);
} }

View file

@ -13,14 +13,11 @@ $color-warning-text: var(--color-warning-text);
$color-info-text: var(--color-info-text); $color-info-text: var(--color-info-text);
$color-success-text: var(--color-success-text); $color-success-text: var(--color-success-text);
$color-text: #110f0f; $color-text: var(--color-text);
$color-text-secondary: rgba($color-text, 0.6); $color-text-secondary: var(--color-text-secondary);
$color-text-disabled: rgba($color-text, 0.4); $color-text-disabled: var(--color-text-disabled);
$color-black: #110f0f; $color-black: #110f0f;
$color-dove-gray: #616161;
$color-silver: #C4C4C4;
$color-alto: #D8D8D8;
$color-white: #ffffff; $color-white: #ffffff;
$color-dodger-blue: #1A90FF; $color-dodger-blue: #1A90FF;
@ -30,8 +27,7 @@ $color-tropical-blue: #C9E3F6;
/************* background *************/ /************* background *************/
$color-background-body: var(--color-background-body); $color-background-body: var(--color-background-body);
$color-background-block: var(--color-background-block); $color-background-block: var(--color-background-block);
$color-background-modal: rgba($color-black, 0.7); $color-background-tooltip: var(--color-background-tooltip);
$color-background-tooltip: rgba($color-dove-gray, 0.92);
/************* padding *************/ /************* padding *************/
@ -51,7 +47,7 @@ $font-size-small: 10px;
/************* border *************/ /************* border *************/
$border-divider: 1px solid rgba($color-black, 0.15); $border-divider: var(--border-divider);
/************* border-radius *************/ /************* border-radius *************/
@ -61,6 +57,7 @@ $border-radius-large: 16px;
/************* box-shadows *************/ /************* box-shadows *************/
$box-shadow: 1px 2px 12px rgba($color-black, 0.08); $box-shadow: var(--box-shadow);
$box-shadow-bottom: rgba($color-black, 0.04) 0px 3px 5px; $box-shadow-popper: var(--box-shadow-popper);
$box-shadow-popper: rgba($color-black, 0.1) 0px 2px 8px 0px;
$color-hover-black: var(--color-hover-black);

View file

@ -7,6 +7,7 @@ export type StorageKeys = "BASIC_AUTH_DATA"
| "SERIES_LIMITS" | "SERIES_LIMITS"
| "TABLE_COMPACT" | "TABLE_COMPACT"
| "TIMEZONE" | "TIMEZONE"
| "DARK_THEME"
export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => { export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
if (value) { if (value) {
@ -15,6 +16,7 @@ export const saveToStorage = (key: StorageKeys, value: string | boolean | Record
} else { } else {
removeFromStorage([key]); removeFromStorage([key]);
} }
window.dispatchEvent(new Event("storage"));
}; };
// TODO: make this aware of data type that is stored // TODO: make this aware of data type that is stored

View file

@ -4,6 +4,7 @@ import { getSecondsFromDuration, roundToMilliseconds } from "../time";
import { AxisRange } from "../../state/graph/reducer"; import { AxisRange } from "../../state/graph/reducer";
import { formatTicks, sizeAxis } from "./helpers"; import { formatTicks, sizeAxis } from "./helpers";
import { TimeParams } from "../../types"; import { TimeParams } from "../../types";
import { getCssVariable } from "../theme";
// see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts // see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
const timeValues = [ const timeValues = [
@ -22,10 +23,11 @@ export const getAxes = (series: Series[], unit?: string): Axis[] => Array.from(n
scale: a, scale: a,
show: true, show: true,
size: sizeAxis, size: sizeAxis,
stroke: getCssVariable("color-text"),
font: "10px Arial", font: "10px Arial",
values: (u: uPlot, ticks: number[]) => formatTicks(u, ticks, unit) values: (u: uPlot, ticks: number[]) => formatTicks(u, ticks, unit)
}; };
if (!a) return { space: 80, values: timeValues }; if (!a) return { space: 80, values: timeValues, stroke: getCssVariable("color-text") };
if (!(Number(a) % 2)) return { ...axis, side: 1 }; if (!(Number(a) % 2)) return { ...axis, side: 1 };
return axis; return axis;
}); });

View file

@ -1,5 +1,6 @@
/* eslint-disable */ /* eslint-disable */
import uPlot from "uplot"; import uPlot from "uplot";
import {getCssVariable} from "../theme";
export const seriesBarsPlugin = (opts) => { export const seriesBarsPlugin = (opts) => {
let pxRatio; let pxRatio;
@ -88,7 +89,7 @@ export const seriesBarsPlugin = (opts) => {
u.ctx.save(); u.ctx.save();
u.ctx.font = font; u.ctx.font = font;
u.ctx.fillStyle = "black"; u.ctx.fillStyle = getCssVariable("color-text");
uPlot.orient(u, sidx, ( uPlot.orient(u, sidx, (
series, series,

View file

@ -15,6 +15,8 @@ The following tip changes can be tested by building VictoriaMetrics components f
## tip ## tip
* 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: [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: [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: add `-internStringMaxLen` command-line flag, which can be used for fine-tuning RAM vs CPU usage in certain workloads. For example, if the stored time series contain long labels, then it may be useful reducing the `-internStringMaxLen` in order to reduce memory usage at the cost of increased CPU usage. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3692). * FEATURE: add `-internStringMaxLen` command-line flag, which can be used for fine-tuning RAM vs CPU usage in certain workloads. For example, if the stored time series contain long labels, then it may be useful reducing the `-internStringMaxLen` in order to reduce memory usage at the cost of increased CPU usage. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3692).