mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
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:
parent
c24c2d20d4
commit
97aa9b03de
59 changed files with 1493 additions and 658 deletions
1246
app/vmui/packages/vmui/package-lock.json
generated
1246
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -17,15 +17,11 @@ const App: FC = () => {
|
|||
|
||||
const [loadingTheme, setLoadingTheme] = useState(true);
|
||||
|
||||
if (loadingTheme) return (
|
||||
<>
|
||||
<Spinner/>
|
||||
<ThemeProvider setLoadingTheme={setLoadingTheme}/>;
|
||||
</>
|
||||
);
|
||||
|
||||
return <>
|
||||
<HashRouter>
|
||||
{loadingTheme && <Spinner/>}
|
||||
<ThemeProvider setLoadingTheme={setLoadingTheme}/>
|
||||
|
||||
<HashRouter key={`${loadingTheme}`}>
|
||||
<AppContextProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
|
|
|
@ -3,11 +3,13 @@ import uPlot, { Options as uPlotOptions } from "uplot";
|
|||
import useResize from "../../../hooks/useResize";
|
||||
import { BarChartProps } from "./types";
|
||||
import "./style.scss";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
const BarChart: FC<BarChartProps> = ({
|
||||
data,
|
||||
container,
|
||||
configs }) => {
|
||||
const { darkTheme } = useAppState();
|
||||
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
|
@ -28,7 +30,7 @@ const BarChart: FC<BarChartProps> = ({
|
|||
const u = new uPlot(options, data, uPlotRef.current);
|
||||
setUPlotInst(u);
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, layoutSize]);
|
||||
}, [uPlotRef.current, layoutSize, darkTheme]);
|
||||
|
||||
useEffect(() => updateChart(), [data]);
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
|||
pointer-events: none;
|
||||
|
||||
&_sticky {
|
||||
background-color: $color-dove-gray;
|
||||
pointer-events: auto;
|
||||
z-index: 99;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import "./style.scss";
|
|||
import classNames from "classnames";
|
||||
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
||||
import dayjs from "dayjs";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
|
@ -47,6 +48,8 @@ const LineChart: FC<LineChartProps> = ({
|
|||
container,
|
||||
height
|
||||
}) => {
|
||||
const { darkTheme } = useAppState();
|
||||
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
|
@ -222,7 +225,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
setUPlotInst(u);
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, series, layoutSize, height]);
|
||||
}, [uPlotRef.current, series, layoutSize, height, darkTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getAppModeEnable } from "../../../utils/app-mode";
|
|||
import classNames from "classnames";
|
||||
import Timezones from "./Timezones/Timezones";
|
||||
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import ThemeControl from "../ThemeControl/ThemeControl";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
|
@ -82,6 +83,9 @@ const GlobalSettings: FC = () => {
|
|||
onChange={setTimezone}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-server-configurator__input">
|
||||
<ThemeControl/>
|
||||
</div>
|
||||
<div className="vm-server-configurator__footer">
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
padding: calc($padding-small/2);
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEn
|
|||
</h3>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
startIcon={<CloseIcon/>}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
|
|
@ -158,7 +158,7 @@ const StepConfigurator: FC = () => {
|
|||
className="vm-link vm-link_colored"
|
||||
href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
Read more about Range query
|
||||
</a>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
padding: 0.2em 0.4em;
|
||||
margin: 0 0.2em;
|
||||
font-size: 85%;
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,11 +13,14 @@ import useResize from "../../../../hooks/useResize";
|
|||
import DatePicker from "../../../Main/DatePicker/DatePicker";
|
||||
import "./style.scss";
|
||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||
import classNames from "classnames";
|
||||
import { useAppState } from "../../../../state/common/StateContext";
|
||||
|
||||
export const TimeSelector: FC = () => {
|
||||
const { darkTheme } = useAppState();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
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 [from, setFrom] = useState<string>();
|
||||
|
@ -120,7 +123,7 @@ export const TimeSelector: FC = () => {
|
|||
|
||||
return <>
|
||||
<div ref={buttonRef}>
|
||||
<Tooltip title="Time range controls">
|
||||
<Tooltip title={displayFullDate ? "Time range controls" : dateTitle}>
|
||||
<Button
|
||||
className={appModeEnable ? "" : "vm-header-button"}
|
||||
variant="contained"
|
||||
|
@ -144,7 +147,12 @@ export const TimeSelector: FC = () => {
|
|||
ref={wrapperRef}
|
||||
>
|
||||
<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
|
||||
className="vm-time-selector-left-inputs__date"
|
||||
ref={fromRef}
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
|
||||
&_dark &__date {
|
||||
border-color: $color-text-disabled;
|
||||
}
|
||||
|
||||
&__date {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 14px;
|
||||
|
@ -70,7 +74,7 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
padding: calc($padding-small/2);
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC } from "preact/compat";
|
||||
import dayjs from "dayjs";
|
||||
import "./style.scss";
|
||||
import { LogoIcon } from "../../Main/Icons";
|
||||
import { IssueIcon, LogoIcon, WikiIcon } from "../../Main/Icons";
|
||||
|
||||
const Footer: FC = () => {
|
||||
const copyrightYears = `2019-${dayjs().format("YYYY")}`;
|
||||
|
@ -11,18 +11,28 @@ const Footer: FC = () => {
|
|||
className="vm-link vm-footer__website"
|
||||
target="_blank"
|
||||
href="https://victoriametrics.com/"
|
||||
rel="noreferrer"
|
||||
rel="me noreferrer"
|
||||
>
|
||||
<LogoIcon/>
|
||||
victoriametrics.com
|
||||
</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"
|
||||
href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new/choose"
|
||||
rel="noreferrer"
|
||||
>
|
||||
create an issue
|
||||
<IssueIcon/>
|
||||
Create an issue
|
||||
</a>
|
||||
<div className="vm-footer__copyright">
|
||||
© {copyrightYears} VictoriaMetrics
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $padding-medium;
|
||||
gap: $padding-large;
|
||||
gap: $padding-medium;
|
||||
border-top: $border-divider;
|
||||
color: $color-text-secondary;
|
||||
background: $color-background-body;
|
||||
|
||||
&__link,
|
||||
&__website {
|
||||
display: grid;
|
||||
grid-template-columns: 12px auto;
|
||||
|
@ -17,6 +19,14 @@
|
|||
gap: 6px;
|
||||
}
|
||||
|
||||
&__website {
|
||||
margin-right: $padding-global;
|
||||
}
|
||||
|
||||
&__link {
|
||||
grid-template-columns: 14px auto;
|
||||
}
|
||||
|
||||
&__copyright {
|
||||
text-align: right;
|
||||
flex-grow: 1;
|
||||
|
|
|
@ -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 { setQueryStringWithoutPageReload } from "../../../utils/query-string";
|
||||
import { TimeSelector } from "../../Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
|
||||
import GlobalSettings from "../../Configurators/GlobalSettings/GlobalSettings";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import router, { RouterOptions, routerOptions } from "../../../router";
|
||||
import { useEffect } from "react";
|
||||
import ShortcutKeys from "../../Main/ShortcutKeys/ShortcutKeys";
|
||||
import { getAppModeEnable, getAppModeParams } from "../../../utils/app-mode";
|
||||
import CardinalityDatePicker from "../../Configurators/CardinalityDatePicker/CardinalityDatePicker";
|
||||
import { LogoFullIcon } from "../../Main/Icons";
|
||||
import { getCssVariable } from "../../../utils/theme";
|
||||
import Tabs from "../../Main/Tabs/Tabs";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { useDashboardsState } from "../../../state/dashboards/DashboardsStateContext";
|
||||
import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import HeaderNav from "./HeaderNav/HeaderNav";
|
||||
|
||||
const Header: FC = () => {
|
||||
const primaryColor = getCssVariable("color-primary");
|
||||
const { darkTheme } = useAppState();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
|
||||
const { headerStyles: {
|
||||
background = appModeEnable ? "#FFF" : primaryColor,
|
||||
color = appModeEnable ? primaryColor : "#FFF",
|
||||
} = {} } = getAppModeParams();
|
||||
const primaryColor = useMemo(() => {
|
||||
const variable = darkTheme ? "color-background-block" : "color-primary";
|
||||
return getCssVariable(variable);
|
||||
}, [darkTheme]);
|
||||
|
||||
const { background, color } = useMemo(() => {
|
||||
const { headerStyles: {
|
||||
background = appModeEnable ? "#FFF" : primaryColor,
|
||||
color = appModeEnable ? primaryColor : "#FFF",
|
||||
} = {} } = getAppModeParams();
|
||||
|
||||
return { background, color };
|
||||
}, [primaryColor]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
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(() => {
|
||||
return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
|
||||
}, [pathname]);
|
||||
|
||||
const onClickLogo = () => {
|
||||
navigateHandler(router.home);
|
||||
navigate({ pathname: router.home, search: search });
|
||||
setQueryStringWithoutPageReload({});
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const navigateHandler = (pathname: string) => {
|
||||
navigate({ pathname, search: search });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(pathname);
|
||||
}, [pathname]);
|
||||
|
||||
return <header
|
||||
className={classNames({
|
||||
"vm-header": true,
|
||||
"vm-header_app": appModeEnable
|
||||
"vm-header_app": appModeEnable,
|
||||
"vm-header_dark": darkTheme
|
||||
})}
|
||||
style={{ background, color }}
|
||||
>
|
||||
|
@ -93,14 +65,10 @@ const Header: FC = () => {
|
|||
<LogoFullIcon/>
|
||||
</div>
|
||||
)}
|
||||
<div className="vm-header-nav">
|
||||
<Tabs
|
||||
isNavLink
|
||||
activeItem={activeMenu}
|
||||
items={routes.filter(r => !r.hide)}
|
||||
color={color}
|
||||
/>
|
||||
</div>
|
||||
<HeaderNav
|
||||
color={color}
|
||||
background={background}
|
||||
/>
|
||||
<div className="vm-header__settings">
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,17 +6,18 @@
|
|||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: $padding-small $padding-medium;
|
||||
gap: $padding-large;
|
||||
gap: 0 $padding-large;
|
||||
z-index: 99;
|
||||
|
||||
&_app {
|
||||
padding: $padding-small 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
gap: $padding-global;
|
||||
|
||||
.vm-tabs {
|
||||
gap: 0;
|
||||
&_dark {
|
||||
.vm-header-button,
|
||||
button:before,
|
||||
button {
|
||||
background-color: $color-background-block;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,11 +44,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-nav {
|
||||
font-size: $font-size-small;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ReactNode } from "react";
|
|||
import classNames from "classnames";
|
||||
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../Icons";
|
||||
import "./style.scss";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
interface AlertProps {
|
||||
variant?: "success" | "error" | "info" | "warning"
|
||||
|
@ -19,12 +20,14 @@ const icons = {
|
|||
const Alert: FC<AlertProps> = ({
|
||||
variant,
|
||||
children }) => {
|
||||
const { darkTheme } = useAppState();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-alert": true,
|
||||
[`vm-alert_${variant}`]: variant
|
||||
[`vm-alert_${variant}`]: variant,
|
||||
"vm-alert_dark": darkTheme
|
||||
})}
|
||||
>
|
||||
<div className="vm-alert__icon">{icons[variant || "info"]}</div>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
border-radius: $border-radius-medium;
|
||||
box-shadow: $box-shadow;
|
||||
font-size: $font-size-medium;
|
||||
font-weight: 500;
|
||||
font-weight: normal;
|
||||
color: $color-text;
|
||||
line-height: 20px;
|
||||
|
||||
|
@ -75,4 +75,14 @@
|
|||
background-color: $color-warning;
|
||||
}
|
||||
}
|
||||
|
||||
&_dark {
|
||||
&:after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&_dark &__content {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ $button-radius: 6px;
|
|||
padding: 6px 14px;
|
||||
font-size: $font-size-small;
|
||||
line-height: 15px;
|
||||
font-weight: 500;
|
||||
font-weight: normal;
|
||||
min-height: 31px;
|
||||
border-radius: $button-radius;
|
||||
color: $color-white;
|
||||
|
@ -21,7 +21,7 @@ $button-radius: 6px;
|
|||
white-space: nowrap;
|
||||
|
||||
&:hover:after {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&:before,
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
transition: color 200ms ease, background-color 300ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&_empty {
|
||||
|
@ -144,7 +144,7 @@
|
|||
transition: color 200ms ease, background-color 300ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&_selected {
|
||||
|
@ -270,6 +270,10 @@
|
|||
justify-content: space-between;
|
||||
margin-top: $padding-global;
|
||||
|
||||
&_dark &__input {
|
||||
border-color: $color-text-disabled;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 $padding-small;
|
||||
}
|
||||
|
@ -277,11 +281,13 @@
|
|||
&__input {
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
border: 1px solid $color-alto;
|
||||
border: $border-divider;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: $font-size-medium;
|
||||
padding: 2px $padding-small;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
color: $color-text;
|
||||
|
||||
&:focus {
|
||||
border-color: $color-primary;
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
|||
import { Dayjs } from "dayjs";
|
||||
import { FormEvent, FocusEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useAppState } from "../../../../state/common/StateContext";
|
||||
|
||||
interface CalendarTimepickerProps {
|
||||
selectDate: Dayjs
|
||||
|
@ -13,6 +14,7 @@ enum TimeUnits { hour, minutes, seconds }
|
|||
|
||||
|
||||
const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onClose }) => {
|
||||
const { darkTheme } = useAppState();
|
||||
|
||||
const [activeField, setActiveField] = useState<TimeUnits>(TimeUnits.hour);
|
||||
const [hours, setHours] = useState(selectDate.format("HH"));
|
||||
|
@ -154,7 +156,12 @@ const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onCl
|
|||
</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
|
||||
className="vm-calendar-time-picker-fields__input"
|
||||
value={hours}
|
||||
|
|
|
@ -344,3 +344,48 @@ export const TimelineIcon = () => (
|
|||
></path>
|
||||
</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>
|
||||
|
||||
);
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
&-track {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
border-radius: $border-radius-small;
|
||||
|
||||
&__thumb {
|
||||
|
|
|
@ -8,7 +8,7 @@ $padding-modal: 22px;
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -16,9 +16,11 @@ $padding-modal: 22px;
|
|||
|
||||
&-content {
|
||||
padding: $padding-modal;
|
||||
background: $color-white;
|
||||
background: $color-background-block;
|
||||
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
||||
border-radius: $border-radius-small;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
|
|
|
@ -3,6 +3,7 @@ import classNames from "classnames";
|
|||
import { ArrowDropDownIcon, CloseIcon } from "../Icons";
|
||||
import { FormEvent, MouseEvent } from "react";
|
||||
import Autocomplete from "../Autocomplete/Autocomplete";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import "./style.scss";
|
||||
|
||||
interface SelectProps {
|
||||
|
@ -26,6 +27,7 @@ const Select: FC<SelectProps> = ({
|
|||
autofocus,
|
||||
onChange
|
||||
}) => {
|
||||
const { darkTheme } = useAppState();
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
||||
|
@ -106,7 +108,12 @@ const Select: FC<SelectProps> = ({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="vm-select">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-select": true,
|
||||
"vm-select_dark": darkTheme
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="vm-select-input"
|
||||
onClick={handleToggleList}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
padding: 2px 2px 2px 6px;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: $font-size;
|
||||
|
@ -60,6 +60,8 @@
|
|||
z-index: 2;
|
||||
min-width: 100px;
|
||||
flex-grow: 1;
|
||||
background-color: transparent;
|
||||
color: $color-text;
|
||||
|
||||
&:placeholder-shown {
|
||||
width: auto;
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
line-height: 2;
|
||||
color: $color-text;
|
||||
text-align: center;
|
||||
background-color: $color-white;
|
||||
background-color: $color-background-body;
|
||||
background-repeat: repeat-x;
|
||||
border: $border-divider;
|
||||
border-radius: 4px;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React, { CSSProperties, FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { getFromStorage } from "../../../utils/storage";
|
||||
|
||||
interface SpinnerProps {
|
||||
containerStyles?: CSSProperties;
|
||||
|
@ -8,7 +10,10 @@ interface SpinnerProps {
|
|||
|
||||
const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => (
|
||||
<div
|
||||
className="vm-spinner"
|
||||
className={classNames({
|
||||
"vm-spinner": true,
|
||||
"vm-spinner_dark": getFromStorage("DARK_THEME")
|
||||
})}
|
||||
style={containerStyles && {}}
|
||||
>
|
||||
<div className="half-circle-spinner">
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
z-index: 99;
|
||||
animation: vm-fade 2s cubic-bezier(0.280, 0.840, 0.420, 1.1);
|
||||
|
||||
&_dark {
|
||||
background-color: rgba($color-black, 0.2);
|
||||
}
|
||||
|
||||
&__message {
|
||||
margin-top: $padding-medium;
|
||||
white-space: pre-line;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { FC, KeyboardEvent, useEffect, useRef, HTMLInputTypeAttribute, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useMemo } from "preact/compat";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import "./style.scss";
|
||||
|
||||
interface TextFieldProps {
|
||||
|
@ -38,6 +39,7 @@ const TextField: FC<TextFieldProps> = ({
|
|||
onFocus,
|
||||
onBlur
|
||||
}) => {
|
||||
const { darkTheme } = useAppState();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
@ -81,6 +83,7 @@ const TextField: FC<TextFieldProps> = ({
|
|||
className={classNames({
|
||||
"vm-text-field": true,
|
||||
"vm-text-field_textarea": type === "textarea",
|
||||
"vm-text-field_dark": darkTheme
|
||||
})}
|
||||
data-replicated-value={value}
|
||||
>
|
||||
|
|
|
@ -67,6 +67,8 @@
|
|||
min-height: 34px;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
color: $color-text;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid $color-primary;
|
||||
|
|
|
@ -2,6 +2,8 @@ import { FC, useEffect } from "preact/compat";
|
|||
import { getContrastColor } from "../../../utils/color";
|
||||
import { getCssVariable, setCssVariable } from "../../../utils/theme";
|
||||
import { AppParams, getAppModeParams } from "../../../utils/app-mode";
|
||||
import { getFromStorage } from "../../../utils/storage";
|
||||
import { darkPalette, lightPalette } from "../../../constants/palette";
|
||||
|
||||
interface StyleVariablesProps {
|
||||
setLoadingTheme: (val: boolean) => void
|
||||
|
@ -27,13 +29,6 @@ export const ThemeProvider: FC<StyleVariablesProps> = ({ setLoadingTheme }) => {
|
|||
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 = () => {
|
||||
colorVariables.forEach(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(() => {
|
||||
setAppModePalette();
|
||||
setScrollbarSize();
|
||||
setContrastText();
|
||||
setTheme();
|
||||
setLoadingTheme(false);
|
||||
|
||||
window.addEventListener("storage", setTheme);
|
||||
return () => {
|
||||
window.removeEventListener("storage", setTheme);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
|
|
|
@ -4,6 +4,7 @@ import Trace from "../Trace";
|
|||
import { ArrowDownIcon } from "../../Main/Icons";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
interface RecursiveProps {
|
||||
trace: Trace;
|
||||
|
@ -15,6 +16,7 @@ interface OpenLevels {
|
|||
}
|
||||
|
||||
const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
|
||||
const { darkTheme } = useAppState();
|
||||
const [openLevels, setOpenLevels] = useState({} as OpenLevels);
|
||||
|
||||
const handleListClick = (level: number) => () => {
|
||||
|
@ -26,7 +28,12 @@ const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
|
|||
const progress = trace.duration / totalMsec * 100;
|
||||
|
||||
return (
|
||||
<div className="vm-nested-nav">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-nested-nav": true,
|
||||
"vm-nested-nav_dark": darkTheme,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="vm-nested-nav-header"
|
||||
onClick={handleListClick(trace.idValue)}
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
border-radius: $border-radius-small;
|
||||
background-color: rgba($color-tropical-blue, 0.4);
|
||||
|
||||
&_dark {
|
||||
background-color: rgba($color-black, 0.1);
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
|
@ -15,7 +19,7 @@
|
|||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
|
|
|
@ -14,5 +14,6 @@
|
|||
transform: translateY(-32px);
|
||||
font-size: $font-size;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
|
37
app/vmui/packages/vmui/src/constants/palette.ts
Normal file
37
app/vmui/packages/vmui/src/constants/palette.ts
Normal 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)"
|
||||
};
|
|
@ -4,11 +4,12 @@ import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
|
|||
import { ErrorTypes } from "../../../types";
|
||||
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
|
||||
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 TextField from "../../../components/Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import { useMemo } from "preact/compat";
|
||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
|
||||
export interface CardinalityConfiguratorProps {
|
||||
onSetHistory: (step: number) => void;
|
||||
|
@ -92,13 +93,13 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
|||
onChange={onFocusLabelChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-cardinality-configurator-controls__item">
|
||||
<Switch
|
||||
label={"Autocomplete"}
|
||||
value={autocomplete}
|
||||
onChange={onChangeAutocomplete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vm-cardinality-configurator-bottom__autocomplete">
|
||||
<Switch
|
||||
label={"Autocomplete"}
|
||||
value={autocomplete}
|
||||
onChange={onChangeAutocomplete}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-cardinality-configurator-bottom">
|
||||
<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>}.
|
||||
Show top {topN} entries per table.
|
||||
</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
|
||||
startIcon={<PlayIcon/>}
|
||||
onClick={onRunQuery}
|
||||
|
|
|
@ -18,12 +18,18 @@
|
|||
|
||||
&-bottom {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
align-items: flex-end;
|
||||
gap: $padding-medium;
|
||||
gap: $padding-small;
|
||||
|
||||
&__info {
|
||||
font-size: $font-size;
|
||||
}
|
||||
|
||||
&__docs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns = [], onCh
|
|||
onClick={handleClose}
|
||||
startIcon={<CloseIcon/>}
|
||||
size="small"
|
||||
variant="text"
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-table-settings-popper-list">
|
||||
|
|
|
@ -2,20 +2,25 @@
|
|||
|
||||
.vm-json-form {
|
||||
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;
|
||||
width: 70vw;
|
||||
max-width: 1000px;
|
||||
max-height: 900px;
|
||||
overflow: hidden;
|
||||
|
||||
&_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 {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 900px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
|
|
|
@ -151,7 +151,7 @@ const TracePage: FC = () => {
|
|||
className="vm-link vm-link_colored"
|
||||
href="https://docs.victoriametrics.com/#query-tracing"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
https://docs.victoriametrics.com/#query-tracing
|
||||
</a>
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import { getDefaultServer } from "../../utils/default-server-url";
|
||||
import { getQueryStringValue } from "../../utils/query-string";
|
||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||
|
||||
export interface AppState {
|
||||
serverUrl: string;
|
||||
tenantId: number;
|
||||
darkTheme: boolean
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| { type: "SET_SERVER", payload: string }
|
||||
| { type: "SET_TENANT_ID", payload: number }
|
||||
| { type: "SET_DARK_THEME", payload: boolean }
|
||||
|
||||
export const initialState: AppState = {
|
||||
serverUrl: getDefaultServer(),
|
||||
tenantId: Number(getQueryStringValue("g0.tenantID", 0)),
|
||||
darkTheme: !!getFromStorage("DARK_THEME")
|
||||
};
|
||||
|
||||
export function reducer(state: AppState, action: Action): AppState {
|
||||
|
@ -27,6 +31,12 @@ export function reducer(state: AppState, action: Action): AppState {
|
|||
...state,
|
||||
tenantId: action.payload
|
||||
};
|
||||
case "SET_DARK_THEME":
|
||||
saveToStorage("DARK_THEME", action.payload);
|
||||
return {
|
||||
...state,
|
||||
darkTheme: action.payload
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
&:hover,
|
||||
&_active {
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&_multiselect {
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: $color-primary;
|
||||
background-color: $color-background-block;
|
||||
padding: $padding-small $padding-small $padding-small $padding-global;
|
||||
border-radius: $border-radius-small $border-radius-small 0 0;
|
||||
color: $color-white;
|
||||
color: $color-text;
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
transition: background-color 200ms ease;
|
||||
|
||||
&:hover:not(&_header) {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&_header {
|
||||
|
@ -43,7 +43,7 @@
|
|||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,8 @@
|
|||
}
|
||||
|
||||
&_gray {
|
||||
color: rgba($color-black, 0.4);
|
||||
color: $color-text;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&_right {
|
||||
|
|
|
@ -9,6 +9,7 @@ html, body, #root {
|
|||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
margin: 0;
|
||||
background-color: $color-background-body;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -54,3 +55,24 @@ input[type=number]::-webkit-outer-spin-button {
|
|||
svg {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -30,4 +30,17 @@
|
|||
/* backgrounds */
|
||||
--color-background-body: #FEFEFF;
|
||||
--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);
|
||||
}
|
||||
|
|
|
@ -13,14 +13,11 @@ $color-warning-text: var(--color-warning-text);
|
|||
$color-info-text: var(--color-info-text);
|
||||
$color-success-text: var(--color-success-text);
|
||||
|
||||
$color-text: #110f0f;
|
||||
$color-text-secondary: rgba($color-text, 0.6);
|
||||
$color-text-disabled: rgba($color-text, 0.4);
|
||||
$color-text: var(--color-text);
|
||||
$color-text-secondary: var(--color-text-secondary);
|
||||
$color-text-disabled: var(--color-text-disabled);
|
||||
|
||||
$color-black: #110f0f;
|
||||
$color-dove-gray: #616161;
|
||||
$color-silver: #C4C4C4;
|
||||
$color-alto: #D8D8D8;
|
||||
$color-white: #ffffff;
|
||||
|
||||
$color-dodger-blue: #1A90FF;
|
||||
|
@ -30,8 +27,7 @@ $color-tropical-blue: #C9E3F6;
|
|||
/************* background *************/
|
||||
$color-background-body: var(--color-background-body);
|
||||
$color-background-block: var(--color-background-block);
|
||||
$color-background-modal: rgba($color-black, 0.7);
|
||||
$color-background-tooltip: rgba($color-dove-gray, 0.92);
|
||||
$color-background-tooltip: var(--color-background-tooltip);
|
||||
|
||||
|
||||
/************* padding *************/
|
||||
|
@ -51,7 +47,7 @@ $font-size-small: 10px;
|
|||
|
||||
|
||||
/************* border *************/
|
||||
$border-divider: 1px solid rgba($color-black, 0.15);
|
||||
$border-divider: var(--border-divider);
|
||||
|
||||
|
||||
/************* border-radius *************/
|
||||
|
@ -61,6 +57,7 @@ $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.1) 0px 2px 8px 0px;
|
||||
$box-shadow: var(--box-shadow);
|
||||
$box-shadow-popper: var(--box-shadow-popper);
|
||||
|
||||
$color-hover-black: var(--color-hover-black);
|
||||
|
|
|
@ -7,6 +7,7 @@ export type StorageKeys = "BASIC_AUTH_DATA"
|
|||
| "SERIES_LIMITS"
|
||||
| "TABLE_COMPACT"
|
||||
| "TIMEZONE"
|
||||
| "DARK_THEME"
|
||||
|
||||
export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
|
||||
if (value) {
|
||||
|
@ -15,6 +16,7 @@ export const saveToStorage = (key: StorageKeys, value: string | boolean | Record
|
|||
} else {
|
||||
removeFromStorage([key]);
|
||||
}
|
||||
window.dispatchEvent(new Event("storage"));
|
||||
};
|
||||
|
||||
// TODO: make this aware of data type that is stored
|
||||
|
|
|
@ -4,6 +4,7 @@ import { getSecondsFromDuration, roundToMilliseconds } from "../time";
|
|||
import { AxisRange } from "../../state/graph/reducer";
|
||||
import { formatTicks, sizeAxis } from "./helpers";
|
||||
import { TimeParams } from "../../types";
|
||||
import { getCssVariable } from "../theme";
|
||||
|
||||
// see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
|
||||
const timeValues = [
|
||||
|
@ -22,10 +23,11 @@ export const getAxes = (series: Series[], unit?: string): Axis[] => Array.from(n
|
|||
scale: a,
|
||||
show: true,
|
||||
size: sizeAxis,
|
||||
stroke: getCssVariable("color-text"),
|
||||
font: "10px Arial",
|
||||
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 };
|
||||
return axis;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable */
|
||||
import uPlot from "uplot";
|
||||
import {getCssVariable} from "../theme";
|
||||
|
||||
export const seriesBarsPlugin = (opts) => {
|
||||
let pxRatio;
|
||||
|
@ -88,7 +89,7 @@ export const seriesBarsPlugin = (opts) => {
|
|||
u.ctx.save();
|
||||
|
||||
u.ctx.font = font;
|
||||
u.ctx.fillStyle = "black";
|
||||
u.ctx.fillStyle = getCssVariable("color-text");
|
||||
|
||||
uPlot.orient(u, sidx, (
|
||||
series,
|
||||
|
|
|
@ -15,6 +15,8 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
|||
|
||||
## 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: 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).
|
||||
|
||||
|
|
Loading…
Reference in a new issue