vmui: add flag for default timezone setting (#5611)

* vmui: add flag for default timezone setting #5375

* vmui: validate timezone before client return

* Update app/vmselect/vmui.go

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2024-01-23 03:11:19 +01:00 committed by GitHub
parent 633e6b48ad
commit eb6def0695
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 191 additions and 18 deletions

View file

@ -3040,4 +3040,6 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules
-vmui.customDashboardsPath string
Optional path to vmui dashboards. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui/packages/vmui/public/dashboards
-vmui.defaultTimezone string
The default timezone to be used in vmui. Timezone must be a valid IANA Time Zone. For example: America/New_York, Europe/Berlin, Etc/GMT+3 or Local. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui#timezone-configuration
```

View file

@ -426,6 +426,14 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
}
return true
}
if path == "/vmui/timezone" {
httpserver.EnableCORS(w, r)
if err := handleVMUITimezone(w); err != nil {
httpserver.Errorf(w, r, "%s", err)
return true
}
return true
}
if strings.HasPrefix(path, "/vmui/") {
if strings.HasPrefix(path, "/vmui/static/") {
// Allow clients caching static contents for long period of time, since it shouldn't change over time.

View file

@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
)
@ -14,6 +15,8 @@ import (
var (
vmuiCustomDashboardsPath = flag.String("vmui.customDashboardsPath", "", "Optional path to vmui dashboards. "+
"See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui/packages/vmui/public/dashboards")
vmuiDefaultTimezone = flag.String("vmui.defaultTimezone", "", "The default timezone to be used in vmui."+
"Timezone must be a valid IANA Time Zone. For example: America/New_York, Europe/Berlin, Etc/GMT+3 or Local")
)
// dashboardSettings represents dashboard settings file struct.
@ -65,6 +68,16 @@ func handleVMUICustomDashboards(w http.ResponseWriter) error {
return nil
}
func handleVMUITimezone(w http.ResponseWriter) error {
tz, err := time.LoadLocation(*vmuiDefaultTimezone)
if err != nil {
return fmt.Errorf("cannot load timezone %q: %w", *vmuiDefaultTimezone, err)
}
response := fmt.Sprintf(`{"timezone": %q}`, tz)
writeSuccessResponse(w, []byte(response))
return nil
}
func writeSuccessResponse(w http.ResponseWriter, data []byte) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")

View file

@ -7,6 +7,7 @@ Web UI for VictoriaMetrics
* [Updating vmui embedded into VictoriaMetrics](#updating-vmui-embedded-into-victoriametrics)
* [Predefined dashboards](#predefined-dashboards)
* [App mode config options](#app-mode-config-options)
* [Timezone configuration](#timezone-configuration)
----
@ -246,3 +247,39 @@ vmui can be used to paste into other applications
```html
<div id="root" data-params='{"serverURL":"http://localhost:8428","useTenantID":true,"headerStyles":{"background":"#FFFFFF","color":"#538DE8"},"palette":{"primary":"#538DE8","secondary":"#F76F8E","error":"#FD151B","warning":"#FFB30F","success":"#7BE622","info":"#0F5BFF"}}'></div>
```
----
## Timezone configuration
vmui's timezone setting offers flexibility in displaying time data. It can be set through a configuration flag and is adjustable within the vmui interface. This feature caters to various user preferences and time zones.
### Default Timezone Setting
#### Via Configuration Flag
- Set the default timezone using the `--vmui.defaultTimezone` flag.
- Accepts a valid IANA Time Zone string (e.g., `America/New_York`, `Europe/Berlin`, `Etc/GMT+3`).
- If the flag is unset or invalid, vmui defaults to the browser's local timezone.
#### User Interface Adjustments
- Users can change the timezone in the vmui interface.
- Any changed setting in the interface overrides the flag's default, persisting for the user.
- The timezone specified in the `--vmui.defaultTimezone` flag is included in the vmui's timezone selection dropdown, aiding user choice.
### Key Points
- **Fallback to Browser's Local Timezone**: If the flag is not set or an invalid timezone is specified, vmui uses the local timezone of the user's browser.
- **User Preference Priority**: User-selected timezones in vmui take precedence over the default set by the flag.
- **Cluster Consistency**: Ensure uniform timezone settings across cluster nodes, but individual user interface selections will always override these defaults.
### Examples
Setting a default timezone, with user options to change:
```
./victoria-metrics --vmui.defaultTimezone="America/New_York"
```
In this scenario, if a user in Berlin accesses vmui without changing settings, it will default to their browser's local timezone (CET). If they select a different timezone in vmui, this choice will override the `"America/New_York"` setting for that user.

View file

@ -29,7 +29,7 @@ const GlobalSettings: FC = () => {
const appModeEnable = getAppModeEnable();
const { serverUrl: stateServerUrl, theme } = useAppState();
const { timezone: stateTimezone } = useTimeState();
const { timezone: stateTimezone, defaultTimezone } = useTimeState();
const { seriesLimits } = useCustomPanelState();
const dispatch = useAppDispatch();
@ -78,6 +78,10 @@ const GlobalSettings: FC = () => {
setServerUrl(stateServerUrl);
}, [stateServerUrl]);
useEffect(() => {
setTimezone(stateTimezone);
}, [stateTimezone]);
const controls = [
{
show: !appModeEnable && !isLogsApp,
@ -100,6 +104,7 @@ const GlobalSettings: FC = () => {
show: true,
component: <Timezones
timezoneState={timezone}
defaultTimezone={defaultTimezone}
onChange={setTimezone}
/>
},

View file

@ -51,6 +51,12 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({
}
}, [enabledStorage]);
useEffect(() => {
if (enabledStorage) {
saveToStorage("SERVER_URL", serverUrl);
}
}, [serverUrl]);
return (
<div>
<div className="vm-server-configurator__title">

View file

@ -12,11 +12,16 @@ import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import useBoolean from "../../../../hooks/useBoolean";
interface TimezonesProps {
timezoneState: string
onChange: (val: string) => void
timezoneState: string;
defaultTimezone?: string;
onChange: (val: string) => void;
}
const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
interface PinnedTimezone extends Timezone {
title: string
}
const Timezones: FC<TimezonesProps> = ({ timezoneState, defaultTimezone, onChange }) => {
const { isMobile } = useDeviceDetect();
const timezones = getTimezoneList();
@ -29,6 +34,24 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
setFalse: handleCloseList,
} = useBoolean(false);
const pinnedTimezones = useMemo(() => [
{
title: `Default time (${defaultTimezone})`,
region: defaultTimezone,
utc: defaultTimezone ? getUTCByTimezone(defaultTimezone) : "UTC"
},
{
title: `Browser Time (${dayjs.tz.guess()})`,
region: dayjs.tz.guess(),
utc: getUTCByTimezone(dayjs.tz.guess())
},
{
title: "UTC (Coordinated Universal Time)",
region: "UTC",
utc: "UTC"
},
].filter(t => t.region) as PinnedTimezone[], [defaultTimezone]);
const searchTimezones = useMemo(() => {
if (!search) return timezones;
try {
@ -40,11 +63,6 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]);
const localTimezone = useMemo(() => ({
region: dayjs.tz.guess(),
utc: getUTCByTimezone(dayjs.tz.guess())
}), []);
const activeTimezone = useMemo(() => ({
region: timezoneState,
utc: getUTCByTimezone(timezoneState)
@ -108,13 +126,16 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
onChange={handleChangeSearch}
/>
</div>
<div
className="vm-timezones-item vm-timezones-list-group-options__item"
onClick={createHandlerSetTimezone(localTimezone)}
>
<div className="vm-timezones-item__title">Browser Time ({localTimezone.region})</div>
<div className="vm-timezones-item__utc">{localTimezone.utc}</div>
</div>
{pinnedTimezones.map((t, i) => t && (
<div
key={`${i}_${t.region}`}
className="vm-timezones-item vm-timezones-list-group-options__item"
onClick={createHandlerSetTimezone(t)}
>
<div className="vm-timezones-item__title">{t.title}</div>
<div className="vm-timezones-item__utc">{t.utc}</div>
</div>
))}
</div>
{timezonesGroups.map(t => (
<div

View file

@ -0,0 +1,59 @@
import { useEffect, useState } from "preact/compat";
import { ErrorTypes } from "../types";
import { useAppState } from "../state/common/StateContext";
import { useTimeDispatch } from "../state/time/TimeStateContext";
import { getFromStorage } from "../utils/storage";
import dayjs from "dayjs";
const disabledDefaultTimezone = Boolean(getFromStorage("DISABLED_DEFAULT_TIMEZONE"));
const useFetchDefaultTimezone = () => {
const { serverUrl } = useAppState();
const timeDispatch = useTimeDispatch();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>("");
const setTimezone = (timezoneStr: string) => {
const timezone = timezoneStr.toLowerCase() === "local" ? dayjs.tz.guess() : timezoneStr;
try {
dayjs().tz(timezone).isValid();
timeDispatch({ type: "SET_DEFAULT_TIMEZONE", payload: timezone });
if (disabledDefaultTimezone) return;
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
} catch (e) {
if (e instanceof Error) setError(`${e.name}: ${e.message}`);
}
};
const fetchDefaultTimezone = async () => {
if (!serverUrl || process.env.REACT_APP_TYPE) return;
setError("");
setIsLoading(true);
try {
const response = await fetch(`${serverUrl}/vmui/timezone`);
const resp = await response.json();
if (response.ok) {
setTimezone(resp.timezone);
setIsLoading(false);
} else {
setError(resp.error);
setIsLoading(false);
}
} catch (e) {
setIsLoading(false);
if (e instanceof Error) setError(`${e.name}: ${e.message}`);
}
};
useEffect(() => {
fetchDefaultTimezone();
}, [serverUrl]);
return { isLoading, error };
};
export default useFetchDefaultTimezone;

View file

@ -7,7 +7,7 @@ import { getAppModeEnable } from "../../utils/app-mode";
import classNames from "classnames";
import Footer from "../Footer/Footer";
import { routerOptions } from "../../router";
import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchDashboards";
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
@ -17,7 +17,7 @@ const AnomalyLayout: FC = () => {
const { pathname } = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
useFetchDashboards();
useFetchDefaultTimezone();
const setDocumentTitle = () => {
const defaultTitle = "vmui for vmanomaly";

View file

@ -8,12 +8,15 @@ import Footer from "../Footer/Footer";
import router, { routerOptions } from "../../router";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import ControlsLogsLayout from "./ControlsLogsLayout";
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
const LogsLayout: FC = () => {
const appModeEnable = getAppModeEnable();
const { isMobile } = useDeviceDetect();
const { pathname } = useLocation();
useFetchDefaultTimezone();
const setDocumentTitle = () => {
const defaultTitle = "vmui for VictoriaLogs";
const routeTitle = routerOptions[router.logs]?.title;

View file

@ -10,6 +10,7 @@ import { routerOptions } from "../../router";
import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchDashboards";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import ControlsMainLayout from "./ControlsMainLayout";
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
const MainLayout: FC = () => {
const appModeEnable = getAppModeEnable();
@ -18,6 +19,7 @@ const MainLayout: FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
useFetchDashboards();
useFetchDefaultTimezone();
const setDocumentTitle = () => {
const defaultTitle = "vmui";

View file

@ -17,6 +17,7 @@ export interface TimeState {
period: TimeParams;
relativeTime?: string;
timezone: string;
defaultTimezone?: string;
}
export type TimeAction =
@ -26,6 +27,7 @@ export type TimeAction =
| { type: "RUN_QUERY"}
| { type: "RUN_QUERY_TO_NOW"}
| { type: "SET_TIMEZONE", payload: string }
| { type: "SET_DEFAULT_TIMEZONE", payload: string }
const timezone = getFromStorage("TIMEZONE") as string || dayjs.tz.guess();
setTimezone(timezone);
@ -90,10 +92,16 @@ export function reducer(state: TimeState, action: TimeAction): TimeState {
case "SET_TIMEZONE":
setTimezone(action.payload);
saveToStorage("TIMEZONE", action.payload);
if (state.defaultTimezone) saveToStorage("DISABLED_DEFAULT_TIMEZONE", action.payload !== state.defaultTimezone);
return {
...state,
timezone: action.payload
};
case "SET_DEFAULT_TIMEZONE":
return {
...state,
defaultTimezone: action.payload
};
default:
throw new Error();
}

View file

@ -4,6 +4,7 @@ export type StorageKeys = "AUTOCOMPLETE"
| "SERIES_LIMITS"
| "TABLE_COMPACT"
| "TIMEZONE"
| "DISABLED_DEFAULT_TIMEZONE"
| "THEME"
| "LOGS_LIMIT"
| "EXPLORE_METRICS_TIPS"

View file

@ -53,6 +53,8 @@ The sandbox cluster installation is running under the constant load generated by
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): rename cmd-line flag `vm-native-disable-retries` to `vm-native-disable-per-metric-migration` to better reflect its meaning.
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add `-vm-native-src-insecure-skip-verify` and `-vm-native-dst-insecure-skip-verify` command-line flags for native protocol. It can be used for skipping TLS certificate verification when connecting to the source or destination addresses.
* FEATURE: [Alerting rules for VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#alerts): add `job` label to `DiskRunsOutOfSpace` alerting rule, so it is easier to understand to which installation the triggered instance belongs.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add `-vmui.defaultTimezone` flag to set a default timezone. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5375) and [these docs](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui#timezone-configuration).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): include UTC in the timezone selection dropdown for standardized time referencing. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5375).
* FEATURE: add [VictoriaMetrics datasource](https://github.com/VictoriaMetrics/grafana-datasource) to docker compose environment. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5363).
* BUGFIX: properly return errors from [export APIs](https://docs.victoriametrics.com/#how-to-export-time-series). Previously these errors were silently suppressed. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5649).

View file

@ -1444,6 +1444,8 @@ Below is the output for `/path/to/vmselect -help`:
Network timeout for RPC connections from vmselect to vmstorage (Linux only). Lower values reduce the maximum query durations when some vmstorage nodes become unavailable because of networking issues. Read more about TCP_USER_TIMEOUT at https://blog.cloudflare.com/when-tcp-sockets-refuse-to-die/ . See also -vmstorageDialTimeout (default 3s)
-vmui.customDashboardsPath string
Optional path to vmui dashboards. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui/packages/vmui/public/dashboards
-vmui.defaultTimezone string
The default timezone to be used in vmui. Timezone must be a valid IANA Time Zone. For example: America/New_York, Europe/Berlin, Etc/GMT+3 or Local. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui#timezone-configuration
```
### List of command-line flags for vmstorage

View file

@ -3043,4 +3043,6 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules
-vmui.customDashboardsPath string
Optional path to vmui dashboards. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui/packages/vmui/public/dashboards
-vmui.defaultTimezone string
The default timezone to be used in vmui. Timezone must be a valid IANA Time Zone. For example: America/New_York, Europe/Berlin, Etc/GMT+3 or Local. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui#timezone-configuration
```

View file

@ -3051,4 +3051,6 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules
-vmui.customDashboardsPath string
Optional path to vmui dashboards. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui/packages/vmui/public/dashboards
-vmui.defaultTimezone string
The default timezone to be used in vmui. Timezone must be a valid IANA Time Zone. For example: America/New_York, Europe/Berlin, Etc/GMT+3 or Local. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui#timezone-configuration
```