mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: improvement the theme (#3731)
* feat: add detect the system theme * fix: change logic fetch tenants * feat: add docs and info to cardinality page --------- Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
080a3e2396
commit
dcc5616126
28 changed files with 427 additions and 201 deletions
|
@ -8,56 +8,57 @@ import DashboardsLayout from "./pages/PredefinedPanels";
|
||||||
import CardinalityPanel from "./pages/CardinalityPanel";
|
import CardinalityPanel from "./pages/CardinalityPanel";
|
||||||
import TopQueries from "./pages/TopQueries";
|
import TopQueries from "./pages/TopQueries";
|
||||||
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
|
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
|
||||||
import Spinner from "./components/Main/Spinner/Spinner";
|
|
||||||
import TracePage from "./pages/TracePage";
|
import TracePage from "./pages/TracePage";
|
||||||
import ExploreMetrics from "./pages/ExploreMetrics";
|
import ExploreMetrics from "./pages/ExploreMetrics";
|
||||||
import PreviewIcons from "./components/Main/Icons/PreviewIcons";
|
import PreviewIcons from "./components/Main/Icons/PreviewIcons";
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
|
|
||||||
const [loadingTheme, setLoadingTheme] = useState(true);
|
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{loadingTheme && <Spinner/>}
|
<HashRouter>
|
||||||
<ThemeProvider setLoadingTheme={setLoadingTheme}/>
|
|
||||||
|
|
||||||
<HashRouter key={`${loadingTheme}`}>
|
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
<Routes>
|
<>
|
||||||
<Route
|
<ThemeProvider onLoaded={setLoadedTheme}/>
|
||||||
path={"/"}
|
{loadedTheme && (
|
||||||
element={<Layout/>}
|
<Routes>
|
||||||
>
|
<Route
|
||||||
<Route
|
path={"/"}
|
||||||
path={router.home}
|
element={<Layout/>}
|
||||||
element={<CustomPanel/>}
|
>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path={router.home}
|
||||||
path={router.metrics}
|
element={<CustomPanel/>}
|
||||||
element={<ExploreMetrics/>}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path={router.metrics}
|
||||||
path={router.cardinality}
|
element={<ExploreMetrics/>}
|
||||||
element={<CardinalityPanel/>}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path={router.cardinality}
|
||||||
path={router.topQueries}
|
element={<CardinalityPanel/>}
|
||||||
element={<TopQueries/>}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path={router.topQueries}
|
||||||
path={router.trace}
|
element={<TopQueries/>}
|
||||||
element={<TracePage/>}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path={router.trace}
|
||||||
path={router.dashboards}
|
element={<TracePage/>}
|
||||||
element={<DashboardsLayout/>}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path={router.dashboards}
|
||||||
path={router.icons}
|
element={<DashboardsLayout/>}
|
||||||
element={<PreviewIcons/>}
|
/>
|
||||||
/>
|
<Route
|
||||||
</Route>
|
path={router.icons}
|
||||||
</Routes>
|
element={<PreviewIcons/>}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</AppContextProvider>
|
</AppContextProvider>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</>;
|
</>;
|
||||||
|
|
|
@ -9,7 +9,7 @@ const BarChart: FC<BarChartProps> = ({
|
||||||
data,
|
data,
|
||||||
container,
|
container,
|
||||||
configs }) => {
|
configs }) => {
|
||||||
const { darkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
|
||||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||||
|
@ -30,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, darkTheme]);
|
}, [uPlotRef.current, layoutSize, isDarkTheme]);
|
||||||
|
|
||||||
useEffect(() => updateChart(), [data]);
|
useEffect(() => updateChart(), [data]);
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
container,
|
container,
|
||||||
height
|
height
|
||||||
}) => {
|
}) => {
|
||||||
const { darkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
|
||||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||||
const [isPanning, setPanning] = useState(false);
|
const [isPanning, setPanning] = useState(false);
|
||||||
|
@ -225,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, darkTheme]);
|
}, [uPlotRef.current, series, layoutSize, height, isDarkTheme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useAppDispatch, useAppState } from "../../../../state/common/StateConte
|
||||||
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
||||||
import { ArrowDownIcon, StorageIcons } from "../../../Main/Icons";
|
import { ArrowDownIcon, StorageIcons } from "../../../Main/Icons";
|
||||||
import Button from "../../../Main/Button/Button";
|
import Button from "../../../Main/Button/Button";
|
||||||
import { useFetchAccountIds } from "./hooks/useFetchAccountIds";
|
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { replaceTenantId } from "../../../../utils/default-server-url";
|
import { replaceTenantId } from "../../../../utils/default-server-url";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
@ -11,13 +10,12 @@ import Popper from "../../../Main/Popper/Popper";
|
||||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||||
|
|
||||||
const TenantsConfiguration: FC = () => {
|
const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
|
||||||
const { tenantId: tenantIdState, serverUrl } = useAppState();
|
const { tenantId: tenantIdState, serverUrl } = useAppState();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const timeDispatch = useTimeDispatch();
|
const timeDispatch = useTimeDispatch();
|
||||||
const { accountIds } = useFetchAccountIds();
|
|
||||||
|
|
||||||
const [openOptions, setOpenOptions] = useState(false);
|
const [openOptions, setOpenOptions] = useState(false);
|
||||||
const optionsButtonRef = useRef<HTMLDivElement>(null);
|
const optionsButtonRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -46,7 +44,6 @@ const TenantsConfiguration: FC = () => {
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
const updateServerUrl = replaceTenantId(serverUrl, tenant);
|
const updateServerUrl = replaceTenantId(serverUrl, tenant);
|
||||||
if (updateServerUrl === serverUrl) return;
|
if (updateServerUrl === serverUrl) return;
|
||||||
console.log("SET_SERVER", updateServerUrl);
|
|
||||||
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
|
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
|
||||||
timeDispatch({ type: "RUN_QUERY" });
|
timeDispatch({ type: "RUN_QUERY" });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
|
||||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||||
|
import { Theme } from "../../../types";
|
||||||
|
import Toggle from "../../Main/Toggle/Toggle";
|
||||||
|
|
||||||
const options = [
|
const options = Object.values(Theme).map(value => ({ title: value, value }));
|
||||||
{ title: "Light", value: false },
|
|
||||||
{ title: "Dark", value: true }
|
|
||||||
];
|
|
||||||
|
|
||||||
const ThemeControl = () => {
|
const ThemeControl = () => {
|
||||||
const { darkTheme } = useAppState();
|
const { theme } = useAppState();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const createHandlerClickItem = (value: boolean) => () => {
|
const handleClickItem = (value: string) => {
|
||||||
dispatch({ type: "SET_DARK_THEME", payload: value });
|
dispatch({ type: "SET_THEME", payload: value as Theme });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -21,21 +18,12 @@ const ThemeControl = () => {
|
||||||
<div className="vm-server-configurator__title">
|
<div className="vm-server-configurator__title">
|
||||||
Theme preferences
|
Theme preferences
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-theme-control-options">
|
<div className="vm-theme-control__toggle">
|
||||||
<div
|
<Toggle
|
||||||
className="vm-theme-control-options__highlight"
|
options={options}
|
||||||
style={{ left: darkTheme ? "50%" : 0 }}
|
value={theme}
|
||||||
|
onChange={handleClickItem}
|
||||||
/>
|
/>
|
||||||
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,44 +1,10 @@
|
||||||
@use "src/styles/variables" as *;
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
.vm-theme-control {
|
.vm-theme-control {
|
||||||
&-options {
|
|
||||||
position: relative;
|
&__toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border: $border-divider;
|
min-width: 300px;
|
||||||
border-radius: $border-radius-medium;
|
text-transform: capitalize;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import classNames from "classnames";
|
||||||
import { useAppState } from "../../../../state/common/StateContext";
|
import { useAppState } from "../../../../state/common/StateContext";
|
||||||
|
|
||||||
export const TimeSelector: FC = () => {
|
export const TimeSelector: FC = () => {
|
||||||
const { darkTheme } = useAppState();
|
const { isDarkTheme } = 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 > 1280, [documentSize]);
|
const displayFullDate = useMemo(() => documentSize.width > 1280, [documentSize]);
|
||||||
|
@ -150,7 +150,7 @@ export const TimeSelector: FC = () => {
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-time-selector-left-inputs": true,
|
"vm-time-selector-left-inputs": true,
|
||||||
"vm-time-selector-left-inputs_dark": darkTheme
|
"vm-time-selector-left-inputs_dark": isDarkTheme
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -16,15 +16,17 @@ import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigura
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
import { useAppState } from "../../../state/common/StateContext";
|
||||||
import HeaderNav from "./HeaderNav/HeaderNav";
|
import HeaderNav from "./HeaderNav/HeaderNav";
|
||||||
import TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
import TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
||||||
|
import { useFetchAccountIds } from "../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
const { darkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
const { accountIds } = useFetchAccountIds();
|
||||||
|
|
||||||
const primaryColor = useMemo(() => {
|
const primaryColor = useMemo(() => {
|
||||||
const variable = darkTheme ? "color-background-block" : "color-primary";
|
const variable = isDarkTheme ? "color-background-block" : "color-primary";
|
||||||
return getCssVariable(variable);
|
return getCssVariable(variable);
|
||||||
}, [darkTheme]);
|
}, [isDarkTheme]);
|
||||||
|
|
||||||
const { background, color } = useMemo(() => {
|
const { background, color } = useMemo(() => {
|
||||||
const { headerStyles: {
|
const { headerStyles: {
|
||||||
|
@ -52,7 +54,7 @@ const Header: FC = () => {
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-header": true,
|
"vm-header": true,
|
||||||
"vm-header_app": appModeEnable,
|
"vm-header_app": appModeEnable,
|
||||||
"vm-header_dark": darkTheme
|
"vm-header_dark": isDarkTheme
|
||||||
})}
|
})}
|
||||||
style={{ background, color }}
|
style={{ background, color }}
|
||||||
>
|
>
|
||||||
|
@ -70,7 +72,7 @@ const Header: FC = () => {
|
||||||
background={background}
|
background={background}
|
||||||
/>
|
/>
|
||||||
<div className="vm-header__settings">
|
<div className="vm-header__settings">
|
||||||
{headerSetup?.tenant && <TenantsConfiguration/>}
|
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds}/>}
|
||||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||||
|
|
|
@ -20,14 +20,14 @@ const icons = {
|
||||||
const Alert: FC<AlertProps> = ({
|
const Alert: FC<AlertProps> = ({
|
||||||
variant,
|
variant,
|
||||||
children }) => {
|
children }) => {
|
||||||
const { darkTheme } = useAppState();
|
const { isDarkTheme } = 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
|
"vm-alert_dark": isDarkTheme
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="vm-alert__icon">{icons[variant || "info"]}</div>
|
<div className="vm-alert__icon">{icons[variant || "info"]}</div>
|
||||||
|
|
|
@ -14,7 +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 { isDarkTheme } = 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"));
|
||||||
|
@ -159,7 +159,7 @@ const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onCl
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-calendar-time-picker-fields": true,
|
"vm-calendar-time-picker-fields": true,
|
||||||
"vm-calendar-time-picker-fields_dark": darkTheme
|
"vm-calendar-time-picker-fields_dark": isDarkTheme
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -27,7 +27,7 @@ const Select: FC<SelectProps> = ({
|
||||||
autofocus,
|
autofocus,
|
||||||
onChange
|
onChange
|
||||||
}) => {
|
}) => {
|
||||||
const { darkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
||||||
|
@ -111,7 +111,7 @@ const Select: FC<SelectProps> = ({
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-select": true,
|
"vm-select": true,
|
||||||
"vm-select_dark": darkTheme
|
"vm-select_dark": isDarkTheme
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
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 classNames from "classnames";
|
||||||
import { getFromStorage } from "../../../utils/storage";
|
import { useAppState } from "../../../state/common/StateContext";
|
||||||
|
|
||||||
interface SpinnerProps {
|
interface SpinnerProps {
|
||||||
containerStyles?: CSSProperties;
|
containerStyles?: CSSProperties;
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => (
|
const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => {
|
||||||
<div
|
const { isDarkTheme } = useAppState();
|
||||||
className={classNames({
|
|
||||||
"vm-spinner": true,
|
return (
|
||||||
"vm-spinner_dark": getFromStorage("DARK_THEME")
|
<div
|
||||||
})}
|
className={classNames({
|
||||||
style={containerStyles && {}}
|
"vm-spinner": true,
|
||||||
>
|
"vm-spinner_dark": isDarkTheme,
|
||||||
<div className="half-circle-spinner">
|
})}
|
||||||
<div className="circle circle-1"></div>
|
style={containerStyles && {}}
|
||||||
<div className="circle circle-2"></div>
|
>
|
||||||
|
<div className="half-circle-spinner">
|
||||||
|
<div className="circle circle-1"></div>
|
||||||
|
<div className="circle circle-2"></div>
|
||||||
|
</div>
|
||||||
|
{message && <div className="vm-spinner__message">{message}</div>}
|
||||||
</div>
|
</div>
|
||||||
{message && <div className="vm-spinner__message">{message}</div>}
|
);
|
||||||
</div>
|
};
|
||||||
);
|
|
||||||
|
|
||||||
export default Spinner;
|
export default Spinner;
|
||||||
|
|
|
@ -39,7 +39,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur
|
onBlur
|
||||||
}) => {
|
}) => {
|
||||||
const { darkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
@ -83,7 +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
|
"vm-text-field_dark": isDarkTheme
|
||||||
})}
|
})}
|
||||||
data-replicated-value={value}
|
data-replicated-value={value}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import { FC, useEffect } from "preact/compat";
|
import { FC, useEffect, useState } from "preact/compat";
|
||||||
import { getContrastColor } from "../../../utils/color";
|
import { getContrastColor } from "../../../utils/color";
|
||||||
import { getCssVariable, setCssVariable } from "../../../utils/theme";
|
import { getCssVariable, isSystemDark, setCssVariable } from "../../../utils/theme";
|
||||||
import { AppParams, getAppModeParams } from "../../../utils/app-mode";
|
import { AppParams, getAppModeParams } from "../../../utils/app-mode";
|
||||||
import { getFromStorage } from "../../../utils/storage";
|
import { getFromStorage } from "../../../utils/storage";
|
||||||
import { darkPalette, lightPalette } from "../../../constants/palette";
|
import { darkPalette, lightPalette } from "../../../constants/palette";
|
||||||
|
import { Theme } from "../../../types";
|
||||||
|
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||||
|
import useSystemTheme from "../../../hooks/useSystemTheme";
|
||||||
|
|
||||||
interface StyleVariablesProps {
|
interface ThemeProviderProps {
|
||||||
setLoadingTheme: (val: boolean) => void
|
onLoaded: (val: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorVariables = [
|
const colorVariables = [
|
||||||
|
@ -18,9 +21,18 @@ const colorVariables = [
|
||||||
"success",
|
"success",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ThemeProvider: FC<StyleVariablesProps> = ({ setLoadingTheme }) => {
|
export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
||||||
|
|
||||||
const { palette = {} } = getAppModeParams();
|
const { palette: paletteAppMode = {} } = getAppModeParams();
|
||||||
|
const { theme } = useAppState();
|
||||||
|
const isDarkTheme = useSystemTheme();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [palette, setPalette] = useState({
|
||||||
|
[Theme.dark]: darkPalette,
|
||||||
|
[Theme.light]: lightPalette,
|
||||||
|
[Theme.system]: isSystemDark() ? darkPalette : lightPalette
|
||||||
|
});
|
||||||
|
|
||||||
const setScrollbarSize = () => {
|
const setScrollbarSize = () => {
|
||||||
const { innerWidth, innerHeight } = window;
|
const { innerWidth, innerHeight } = window;
|
||||||
|
@ -30,16 +42,21 @@ export const ThemeProvider: FC<StyleVariablesProps> = ({ setLoadingTheme }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setContrastText = () => {
|
const setContrastText = () => {
|
||||||
colorVariables.forEach(variable => {
|
colorVariables.forEach((variable, i) => {
|
||||||
const color = getCssVariable(`color-${variable}`);
|
const color = getCssVariable(`color-${variable}`);
|
||||||
const text = getContrastColor(color);
|
const text = getContrastColor(color);
|
||||||
setCssVariable(`${variable}-text`, text);
|
setCssVariable(`${variable}-text`, text);
|
||||||
|
|
||||||
|
if (i === colorVariables.length - 1) {
|
||||||
|
dispatch({ type: "SET_DARK_THEME" });
|
||||||
|
onLoaded(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setAppModePalette = () => {
|
const setAppModePalette = () => {
|
||||||
colorVariables.forEach(variable => {
|
colorVariables.forEach(variable => {
|
||||||
const colorFromAppMode = palette[variable as keyof AppParams["palette"]];
|
const colorFromAppMode = paletteAppMode[variable as keyof AppParams["palette"]];
|
||||||
if (colorFromAppMode) setCssVariable(`color-${variable}`, colorFromAppMode);
|
if (colorFromAppMode) setCssVariable(`color-${variable}`, colorFromAppMode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,25 +64,33 @@ export const ThemeProvider: FC<StyleVariablesProps> = ({ setLoadingTheme }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTheme = () => {
|
const setTheme = () => {
|
||||||
const darkTheme = getFromStorage("DARK_THEME");
|
const theme = (getFromStorage("THEME") || Theme.system) as Theme;
|
||||||
const palette = darkTheme ? darkPalette : lightPalette;
|
const result = palette[theme];
|
||||||
Object.entries(palette).forEach(([variable, value]) => {
|
Object.entries(result).forEach(([variable, value]) => {
|
||||||
setCssVariable(variable, value);
|
setCssVariable(variable, value);
|
||||||
});
|
});
|
||||||
setContrastText();
|
setContrastText();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePalette = () => {
|
||||||
|
const newSystemPalette = isSystemDark() ? darkPalette : lightPalette;
|
||||||
|
if (palette[Theme.system] === newSystemPalette) {
|
||||||
|
setTheme();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPalette(prev => ({
|
||||||
|
...prev,
|
||||||
|
[Theme.system]: newSystemPalette
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAppModePalette();
|
setAppModePalette();
|
||||||
setScrollbarSize();
|
setScrollbarSize();
|
||||||
setTheme();
|
setTheme();
|
||||||
setLoadingTheme(false);
|
}, [palette]);
|
||||||
|
|
||||||
window.addEventListener("storage", setTheme);
|
useEffect(updatePalette, [theme, isDarkTheme]);
|
||||||
return () => {
|
|
||||||
window.removeEventListener("storage", setTheme);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
94
app/vmui/packages/vmui/src/components/Main/Toggle/Toggle.tsx
Normal file
94
app/vmui/packages/vmui/src/components/Main/Toggle/Toggle.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import "./style.scss";
|
||||||
|
|
||||||
|
interface ToggleProps {
|
||||||
|
options: {value: string, title?: string, icon?: ReactNode}[]
|
||||||
|
value: string
|
||||||
|
onChange: (val: string) => void
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Toggle: FC<ToggleProps> = ({ options, value, label, onChange }) => {
|
||||||
|
|
||||||
|
const activeRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [position, setPosition] = useState({
|
||||||
|
width: "0px",
|
||||||
|
left: "0px",
|
||||||
|
borderRadius: "0px"
|
||||||
|
});
|
||||||
|
|
||||||
|
const createHandlerChange = (value: string) => () => {
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeRef.current) {
|
||||||
|
setPosition({
|
||||||
|
width: "0px",
|
||||||
|
left: "0px",
|
||||||
|
borderRadius: "0px"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = options.findIndex(o => o.value === value);
|
||||||
|
const { width: widthRect } = activeRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
let width = widthRect;
|
||||||
|
let left = index * width;
|
||||||
|
let borderRadius = "0";
|
||||||
|
if (index === 0) borderRadius = "16px 0 0 16px";
|
||||||
|
|
||||||
|
if (index === options.length - 1) {
|
||||||
|
borderRadius = "10px";
|
||||||
|
left -= 1;
|
||||||
|
borderRadius = "0 16px 16px 0";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index !== 0 && (index !== options.length - 1)) {
|
||||||
|
width += 1;
|
||||||
|
left -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setPosition({ width: `${width}px`, left: `${left}px`, borderRadius });
|
||||||
|
}, [activeRef, value, options]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="vm-toggles">
|
||||||
|
{label && (
|
||||||
|
<label className="vm-toggles__label">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="vm-toggles-group"
|
||||||
|
style={{ gridTemplateColumns: `repeat(${options.length}, 1fr)` }}
|
||||||
|
>
|
||||||
|
{position.borderRadius && <div
|
||||||
|
className="vm-toggles-group__highlight"
|
||||||
|
style={position}
|
||||||
|
/>}
|
||||||
|
{options.map((option, i) => (
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-toggles-group-item": true,
|
||||||
|
"vm-toggles-group-item_first": i === 0,
|
||||||
|
"vm-toggles-group-item_active": option.value === value,
|
||||||
|
"vm-toggles-group-item_icon": option.icon && option.title
|
||||||
|
})}
|
||||||
|
onClick={createHandlerChange(option.value)}
|
||||||
|
key={option.value}
|
||||||
|
ref={option.value === value ? activeRef : null}
|
||||||
|
>
|
||||||
|
{option.icon}
|
||||||
|
{option.title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toggle;
|
81
app/vmui/packages/vmui/src/components/Main/Toggle/style.scss
Normal file
81
app/vmui/packages/vmui/src/components/Main/Toggle/style.scss
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-toggles {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
padding: 0 $padding-global;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-group {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: $padding-small;
|
||||||
|
border-right: $border-divider;
|
||||||
|
border-top: $border-divider;
|
||||||
|
border-bottom: $border-divider;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
transition: color 150ms ease-in;
|
||||||
|
z-index: 2;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&_first {
|
||||||
|
border-radius: 16px 0 0 16px;
|
||||||
|
border-left: $border-divider
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 16px 16px 0;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_icon {
|
||||||
|
grid-template-columns: 14px auto;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_active {
|
||||||
|
color: $color-primary;
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__highlight {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba($color-primary, 0.08);
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
transition: left 200ms cubic-bezier(0.280, 0.840, 0.420, 1), border-radius 200ms linear;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ interface OpenLevels {
|
||||||
}
|
}
|
||||||
|
|
||||||
const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
|
const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
|
||||||
const { darkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
const [openLevels, setOpenLevels] = useState({} as OpenLevels);
|
const [openLevels, setOpenLevels] = useState({} as OpenLevels);
|
||||||
|
|
||||||
const handleListClick = (level: number) => () => {
|
const handleListClick = (level: number) => () => {
|
||||||
|
@ -31,7 +31,7 @@ const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-nested-nav": true,
|
"vm-nested-nav": true,
|
||||||
"vm-nested-nav_dark": darkTheme,
|
"vm-nested-nav_dark": isDarkTheme,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -7,7 +7,7 @@ export const darkPalette = {
|
||||||
"color-success": "#57ab5a",
|
"color-success": "#57ab5a",
|
||||||
"color-background-body": "#22272e",
|
"color-background-body": "#22272e",
|
||||||
"color-background-block": "#2d333b",
|
"color-background-block": "#2d333b",
|
||||||
"color-background-tooltip": "rgba(22, 22, 22, 0.6)",
|
"color-background-tooltip": "rgba(22, 22, 22, 0.8)",
|
||||||
"color-text": "#cdd9e5",
|
"color-text": "#cdd9e5",
|
||||||
"color-text-secondary": "#768390",
|
"color-text-secondary": "#768390",
|
||||||
"color-text-disabled": "#636e7b",
|
"color-text-disabled": "#636e7b",
|
||||||
|
|
20
app/vmui/packages/vmui/src/hooks/useSystemTheme.ts
Normal file
20
app/vmui/packages/vmui/src/hooks/useSystemTheme.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { useEffect, useState } from "preact/compat";
|
||||||
|
import { isSystemDark } from "../utils/theme";
|
||||||
|
|
||||||
|
const useThemeDetector = () => {
|
||||||
|
const [isDarkTheme, setIsDarkTheme] = useState(isSystemDark());
|
||||||
|
|
||||||
|
const mqListener = ((e: MediaQueryListEvent) => {
|
||||||
|
setIsDarkTheme(e.matches);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
darkThemeMq.addEventListener("change", mqListener);
|
||||||
|
return () => darkThemeMq.removeEventListener("change", mqListener);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isDarkTheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useThemeDetector;
|
|
@ -1,14 +1,13 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC, useMemo } from "react";
|
||||||
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
|
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
|
||||||
import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
|
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, QuestionIcon } from "../../../components/Main/Icons";
|
import { InfoIcon, PlayIcon, QuestionIcon, WikiIcon } 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 Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||||
|
|
||||||
export interface CardinalityConfiguratorProps {
|
export interface CardinalityConfiguratorProps {
|
||||||
|
@ -65,7 +64,7 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
||||||
<div className="vm-cardinality-configurator-controls">
|
<div className="vm-cardinality-configurator-controls">
|
||||||
<div className="vm-cardinality-configurator-controls__query">
|
<div className="vm-cardinality-configurator-controls__query">
|
||||||
<QueryEditor
|
<QueryEditor
|
||||||
value={query || match || ""}
|
value={query}
|
||||||
autocomplete={autocomplete}
|
autocomplete={autocomplete}
|
||||||
options={queryOptions}
|
options={queryOptions}
|
||||||
error={error}
|
error={error}
|
||||||
|
@ -91,10 +90,22 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
||||||
type="text"
|
type="text"
|
||||||
value={focusLabel || ""}
|
value={focusLabel || ""}
|
||||||
onChange={onFocusLabelChange}
|
onChange={onFocusLabelChange}
|
||||||
|
endIcon={(
|
||||||
|
<Tooltip
|
||||||
|
title={(
|
||||||
|
<div>
|
||||||
|
<p>To identify values with the highest number of series for the selected label.</p>
|
||||||
|
<p>Adds a table showing the series with the highest number of series.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<InfoIcon/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-cardinality-configurator-bottom__autocomplete">
|
<div className="vm-cardinality-configurator-additional">
|
||||||
<Switch
|
<Switch
|
||||||
label={"Autocomplete"}
|
label={"Autocomplete"}
|
||||||
value={autocomplete}
|
value={autocomplete}
|
||||||
|
@ -108,17 +119,22 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
||||||
Show top {topN} entries per table.
|
Show top {topN} entries per table.
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
className="vm-cardinality-configurator-bottom__docs"
|
className="vm-link vm-link_with-icon"
|
||||||
href="https://victoriametrics.com/blog/cardinality-explorer/"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
href="https://docs.victoriametrics.com/#cardinality-explorer"
|
||||||
rel="help noreferrer"
|
rel="help noreferrer"
|
||||||
>
|
>
|
||||||
<Tooltip title="Example of using">
|
<WikiIcon/>
|
||||||
<Button
|
Documentation
|
||||||
variant="text"
|
</a>
|
||||||
startIcon={<QuestionIcon/>}
|
<a
|
||||||
/>
|
className="vm-link vm-link_with-icon"
|
||||||
</Tooltip>
|
target="_blank"
|
||||||
|
href="https://victoriametrics.com/blog/cardinality-explorer/"
|
||||||
|
rel="help noreferrer"
|
||||||
|
>
|
||||||
|
<QuestionIcon/>
|
||||||
|
Example of using
|
||||||
</a>
|
</a>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<PlayIcon/>}
|
startIcon={<PlayIcon/>}
|
||||||
|
|
|
@ -16,20 +16,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-additional {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
&-bottom {
|
&-bottom {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr auto auto;
|
align-items: center;
|
||||||
align-items: flex-end;
|
gap: $padding-global;
|
||||||
gap: $padding-small;
|
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
|
flex-grow: 1;
|
||||||
font-size: $font-size;
|
font-size: $font-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__docs {
|
a {
|
||||||
display: flex;
|
color: $color-text-secondary;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,6 @@ const Index: FC = () => {
|
||||||
cardinalityDispatch({ type: "RUN_QUERY" });
|
cardinalityDispatch({ type: "RUN_QUERY" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSetQuery = (query: string) => {
|
|
||||||
setQuery(query);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSetHistory = (step: number) => {
|
const onSetHistory = (step: number) => {
|
||||||
const newIndexHistory = queryHistoryIndex + step;
|
const newIndexHistory = queryHistoryIndex + step;
|
||||||
if (newIndexHistory < 0 || newIndexHistory >= queryHistory.length) return;
|
if (newIndexHistory < 0 || newIndexHistory >= queryHistory.length) return;
|
||||||
|
@ -86,7 +82,7 @@ const Index: FC = () => {
|
||||||
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
|
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
|
||||||
focusLabel={focusLabel}
|
focusLabel={focusLabel}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
onSetQuery={onSetQuery}
|
onSetQuery={setQuery}
|
||||||
onSetHistory={onSetHistory}
|
onSetHistory={onSetHistory}
|
||||||
onTopNChange={onTopNChange}
|
onTopNChange={onTopNChange}
|
||||||
onFocusLabelChange={onFocusLabelChange}
|
onFocusLabelChange={onFocusLabelChange}
|
||||||
|
|
|
@ -8,16 +8,18 @@ const router = {
|
||||||
icons: "/icons"
|
icons: "/icons"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface RouterOptionsHeader {
|
||||||
|
tenant?: boolean,
|
||||||
|
stepControl?: boolean,
|
||||||
|
timeSelector?: boolean,
|
||||||
|
executionControls?: boolean,
|
||||||
|
globalSettings?: boolean,
|
||||||
|
cardinalityDatePicker?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface RouterOptions {
|
export interface RouterOptions {
|
||||||
title?: string,
|
title?: string,
|
||||||
header: {
|
header: RouterOptionsHeader
|
||||||
tenant?: boolean,
|
|
||||||
stepControl?: boolean,
|
|
||||||
timeSelector?: boolean,
|
|
||||||
executionControls?: boolean,
|
|
||||||
globalSettings?: boolean,
|
|
||||||
cardinalityDatePicker?: boolean
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const routerOptionsDefault = {
|
const routerOptionsDefault = {
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
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";
|
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||||
|
import { Theme } from "../../types";
|
||||||
|
import { isDarkTheme } from "../../utils/theme";
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
darkTheme: boolean
|
theme: Theme;
|
||||||
|
isDarkTheme: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| { type: "SET_SERVER", payload: string }
|
| { type: "SET_SERVER", payload: string }
|
||||||
| { type: "SET_DARK_THEME", payload: boolean }
|
| { type: "SET_THEME", payload: Theme }
|
||||||
| { type: "SET_TENANT_ID", payload: string }
|
| { type: "SET_TENANT_ID", payload: string }
|
||||||
|
| { type: "SET_DARK_THEME" }
|
||||||
|
|
||||||
const tenantId = getQueryStringValue("g0.tenantID", "") as string;
|
const tenantId = getQueryStringValue("g0.tenantID", "") as string;
|
||||||
|
|
||||||
export const initialState: AppState = {
|
export const initialState: AppState = {
|
||||||
serverUrl: getDefaultServer(tenantId),
|
serverUrl: getDefaultServer(tenantId),
|
||||||
tenantId,
|
tenantId,
|
||||||
darkTheme: !!getFromStorage("DARK_THEME")
|
theme: (getFromStorage("THEME") || Theme.system) as Theme,
|
||||||
|
isDarkTheme: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export function reducer(state: AppState, action: Action): AppState {
|
export function reducer(state: AppState, action: Action): AppState {
|
||||||
|
@ -33,11 +38,16 @@ export function reducer(state: AppState, action: Action): AppState {
|
||||||
...state,
|
...state,
|
||||||
tenantId: action.payload
|
tenantId: action.payload
|
||||||
};
|
};
|
||||||
case "SET_DARK_THEME":
|
case "SET_THEME":
|
||||||
saveToStorage("DARK_THEME", action.payload);
|
saveToStorage("THEME", action.payload);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
darkTheme: action.payload
|
theme: action.payload,
|
||||||
|
};
|
||||||
|
case "SET_DARK_THEME":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isDarkTheme: isDarkTheme(state.theme)
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
throw new Error();
|
throw new Error();
|
||||||
|
|
|
@ -8,6 +8,14 @@
|
||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_with-icon {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
grid-template-columns: 14px auto;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
|
@ -119,3 +119,9 @@ export interface GraphSize {
|
||||||
isDefault?: boolean,
|
isDefault?: boolean,
|
||||||
height: () => number
|
height: () => number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Theme {
|
||||||
|
system = "system",
|
||||||
|
light = "light",
|
||||||
|
dark = "dark",
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ export type StorageKeys = "BASIC_AUTH_DATA"
|
||||||
| "SERIES_LIMITS"
|
| "SERIES_LIMITS"
|
||||||
| "TABLE_COMPACT"
|
| "TABLE_COMPACT"
|
||||||
| "TIMEZONE"
|
| "TIMEZONE"
|
||||||
| "DARK_THEME"
|
| "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) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Theme } from "../types";
|
||||||
|
|
||||||
export const getCssVariable = (variable: string) => {
|
export const getCssVariable = (variable: string) => {
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`);
|
return getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`);
|
||||||
};
|
};
|
||||||
|
@ -5,3 +7,8 @@ export const getCssVariable = (variable: string) => {
|
||||||
export const setCssVariable = (variable: string, value: string) => {
|
export const setCssVariable = (variable: string, value: string) => {
|
||||||
document.documentElement.style.setProperty(`--${variable}`, value);
|
document.documentElement.style.setProperty(`--${variable}`, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isSystemDark = () => window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
|
||||||
|
export const isDarkTheme = (theme: Theme) => (theme === Theme.system && isSystemDark()) || theme === Theme.dark;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue