Skip to content

Commit

Permalink
ui: display configurable timezone in DB Console
Browse files Browse the repository at this point in the history
This commit makes it so all timestamps displayed in
DB Console (with the exception of Advanced Debug pages) use the timezone
set by the cluster setting `ui.display_timezone`.

Only Coordinated Universal Time and America/New_York are supported.
Supporting additional timezones can be achieved by adding more
timezones to the enum passed to `ui.display_timezone`
when the setting is registered, and modifying the webpack configs
to make sure the relevant timezone data is included. See cockroachdb#99848
for more details.

Epic: https://cockroachlabs.atlassian.net/browse/CRDB-5536
Release note (ui): Added the ability for users to view timestamps
in DB Console in their preferred timezone via the cluster setting
`ui.display_timezone`. Currently supported timezones are Coordinated
Universal Time and America/New_York.
  • Loading branch information
Zach Lite committed Apr 18, 2023
1 parent b29901b commit 0aeeafb
Show file tree
Hide file tree
Showing 75 changed files with 1,018 additions and 277 deletions.
4 changes: 2 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/contexts/timezoneContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { createContext, useContext } from "react";
export const CoordinatedUniversalTime = "Etc/UTC";
export const TimezoneContext = createContext<string>(CoordinatedUniversalTime);

interface WithTimezoneProps {
export interface WithTimezoneProps {
timezone: string;
}

Expand All @@ -23,7 +23,7 @@ interface WithTimezoneProps {
export function WithTimezone<T>(
Component: React.ComponentType<T & WithTimezoneProps>,
) {
return (props: T) => {
return (props: React.PropsWithChildren<T>) => {
// This lambda is a React function component.
// It is safe to call a hook here.
// eslint-disable-next-line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React from "react";
import React, { useContext } from "react";
import { Link, RouteComponentProps } from "react-router-dom";
import { Tooltip } from "antd";
import "antd/lib/tooltip/style";
Expand Down Expand Up @@ -51,6 +51,7 @@ import {
} from "src/queryFilter";
import { UIConfigState } from "src/store";
import { TableStatistics } from "src/tableStatistics";
import { Timestamp, Timezone } from "../timestamp";

const cx = classNames.bind(styles);
const sortableTableCx = classNames.bind(sortableTableStyles);
Expand Down Expand Up @@ -611,13 +612,18 @@ export class DatabaseDetailsPage extends React.Component<
placement="bottom"
title="The last time table statistics were created or updated."
>
Table Stats Last Updated (UTC)
Table Stats Last Updated (<Timezone />)
</Tooltip>
),
cell: table =>
!table.details.statsLastUpdated
? "No table statistics found"
: table.details.statsLastUpdated.format(DATE_FORMAT),
!table.details.statsLastUpdated ? (
"No table statistics found"
) : (
<Timestamp
time={table.details.statsLastUpdated}
format={DATE_FORMAT}
/>
),
sort: table => table.details.statsLastUpdated,
className: cx("database-table__col--table-stats"),
name: "tableStatsUpdated",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ import {
} from "src/summaryCard";
import * as format from "src/util/format";
import {
DATE_FORMAT,
DATE_FORMAT_24_UTC,
DATE_FORMAT_24_TZ,
EncodeDatabaseTableUri,
EncodeDatabaseUri,
EncodeUriName,
Expand Down Expand Up @@ -61,6 +60,7 @@ import LoadingError from "../sqlActivity/errorComponent";
import { Loading } from "../loading";
import { UIConfigState } from "../store";
import { QuoteIdentifier } from "../api/safesql";
import { Timestamp } from "../timestamp";

const cx = classNames.bind(styles);
const booleanSettingCx = classnames.bind(booleanSettingStyles);
Expand Down Expand Up @@ -306,21 +306,28 @@ export class DatabaseTablePage extends React.Component<
private getLastResetString() {
const lastReset = this.props.indexStats.lastReset;
if (lastReset.isSame(this.minDate)) {
return "Last reset: Never";
return <>Last reset: Never</>;
} else {
return "Last reset: " + lastReset.format(DATE_FORMAT_24_UTC);
return (
<>
Last reset: <Timestamp time={lastReset} format={DATE_FORMAT_24_TZ} />
</>
);
}
}

private getLastUsedString(indexStat: IndexStat) {
// This case only occurs when we have no reads, resets, or creation time on
// the index.
if (indexStat.lastUsed.isSame(this.minDate)) {
return "Never";
return <>Never</>;
}
return `Last ${indexStat.lastUsedType}: ${indexStat.lastUsed.format(
DATE_FORMAT,
)}`;
return (
<>
Last {indexStat.lastUsedType}:{" "}
<Timestamp time={indexStat.lastUsed} format={DATE_FORMAT_24_TZ} />
</>
);
}

private renderIndexRecommendations = (
Expand Down Expand Up @@ -420,7 +427,7 @@ export class DatabaseTablePage extends React.Component<
},
{
name: "last used",
title: "Last Used (UTC)",
title: "Last Used",
hideTitleUnderline: true,
className: cx("index-stats-table__col-last-used"),
cell: indexStat => this.getLastUsedString(indexStat),
Expand Down Expand Up @@ -566,9 +573,12 @@ export class DatabaseTablePage extends React.Component<
{this.props.details.statsLastUpdated && (
<SummaryCardItem
label="Table Stats Last Updated"
value={this.props.details.statsLastUpdated.format(
DATE_FORMAT_24_UTC,
)}
value={
<Timestamp
time={this.props.details.statsLastUpdated}
format={DATE_FORMAT_24_TZ}
/>
}
/>
)}
{this.props.automaticStatsCollectionEnabled !=
Expand Down
23 changes: 17 additions & 6 deletions pkg/ui/workspaces/cluster-ui/src/dateRangeMenu/dateRangeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React, { useState } from "react";
import React, { useContext, useState } from "react";
import { Alert, DatePicker, Icon, TimePicker } from "antd";
import "antd/lib/time-picker/style";
import "antd/lib/icon/style";
Expand All @@ -21,6 +21,7 @@ import { Button } from "src/button";
import { Text, TextTypes } from "src/text";

import styles from "./dateRangeMenu.module.scss";
import { CoordinatedUniversalTime, TimezoneContext } from "../contexts";

const cx = classNames.bind(styles);

Expand All @@ -44,6 +45,8 @@ export function DateRangeMenu({
onCancel,
onReturnToPresetOptionsClick,
}: DateRangeMenuProps): React.ReactElement {
const timezone = useContext(TimezoneContext);

/**
* Local startMoment and endMoment state are stored here so that users can change the time before clicking "Apply".
* They are re-initialized to startInit and endInit by re-mounting this component. It is thus the responsibility of
Expand All @@ -66,9 +69,11 @@ export function DateRangeMenu({
* the parent component to re-initialize this.
*/
const [startMoment, setStartMoment] = useState<Moment>(
startInit || moment.utc(),
startInit ? startInit.tz(timezone) : moment.tz(timezone),
);
const [endMoment, setEndMoment] = useState<Moment>(
endInit ? endInit.tz(timezone) : moment.tz(timezone),
);
const [endMoment, setEndMoment] = useState<Moment>(endInit || moment.utc());

const onChangeStart = (m?: Moment) => {
m && setStartMoment(m);
Expand Down Expand Up @@ -99,9 +104,15 @@ export function DateRangeMenu({
const isValid = errorMessage === undefined;

const onApply = (): void => {
onSubmit(startMoment, endMoment);
// Idempotently set the start and end moments to UTC.
onSubmit(startMoment.utc(), endMoment.utc());
};

const timezoneLabel =
timezone.toLowerCase() === CoordinatedUniversalTime.toLowerCase()
? "UTC"
: timezone;

return (
<div className={cx("popup-content")}>
<div className={cx("return-to-preset-options-wrapper")}>
Expand All @@ -111,7 +122,7 @@ export function DateRangeMenu({
</a>
</div>
<Text className={cx("label")} textType={TextTypes.BodyStrong}>
Start (UTC)
Start ({timezoneLabel})
</Text>
<DatePicker
disabledDate={isDisabled}
Expand All @@ -130,7 +141,7 @@ export function DateRangeMenu({
/>
<div className={cx("divider")} />
<Text className={cx("label")} textType={TextTypes.BodyStrong}>
End (UTC)
End ({timezoneLabel})
</Text>
<DatePicker
allowClear={false}
Expand Down
5 changes: 4 additions & 1 deletion pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@ export const getStackedBarOpts = (
yAxisDomain: AxisDomain,
yyAxisUnits: AxisUnits,
colourPalette = seriesPalette,
timezone: string,
): Options => {
const options = getBarChartOpts(
userOptions,
xAxisDomain,
yAxisDomain,
yyAxisUnits,
colourPalette,
timezone,
);

options.bands = getStackedBands(unstackedData, () => false);
Expand Down Expand Up @@ -141,6 +143,7 @@ export const getBarChartOpts = (
yAxisDomain: AxisDomain,
yAxisUnits: AxisUnits,
colourPalette = seriesPalette,
timezone: string,
): Options => {
const { series, ...providedOpts } = userOptions;
const defaultBars = getBarsBuilder(0.9, 80);
Expand Down Expand Up @@ -191,7 +194,7 @@ export const getBarChartOpts = (
...s,
})),
],
plugins: [barTooltipPlugin(yAxisUnits)],
plugins: [barTooltipPlugin(yAxisUnits, timezone)],
};

const combinedOpts = merge(opts, providedOpts);
Expand Down
6 changes: 5 additions & 1 deletion pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React, { useEffect, useRef } from "react";
import React, { useContext, useEffect, useRef } from "react";
import classNames from "classnames/bind";
import { getStackedBarOpts, stack } from "./bars";
import uPlot, { AlignedData } from "uplot";
Expand All @@ -20,6 +20,7 @@ import {
calculateYAxisDomain,
} from "../utils/domain";
import { Options } from "uplot";
import { TimezoneContext } from "../../contexts";

const cx = classNames.bind(styles);

Expand All @@ -46,6 +47,7 @@ export const BarGraphTimeSeries: React.FC<BarGraphTimeSeriesProps> = ({
const graphRef = useRef<HTMLDivElement>(null);
const samplingIntervalMillis =
alignedData[0].length > 1 ? alignedData[0][1] - alignedData[0][0] : 1e3;
const timezone = useContext(TimezoneContext);

useEffect(() => {
if (!alignedData) return;
Expand All @@ -69,6 +71,7 @@ export const BarGraphTimeSeries: React.FC<BarGraphTimeSeriesProps> = ({
yAxisDomain,
yAxisUnits,
colourPalette,
timezone,
);

const plot = new uPlot(opts, stackedData, graphRef.current);
Expand All @@ -82,6 +85,7 @@ export const BarGraphTimeSeries: React.FC<BarGraphTimeSeriesProps> = ({
uPlotOptions,
yAxisUnits,
samplingIntervalMillis,
timezone,
]);

return (
Expand Down
20 changes: 16 additions & 4 deletions pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@
// licenses/APL.txt.

import uPlot, { Plugin } from "uplot";
import { AxisUnits, formatTimeStamp } from "../utils/domain";
import { Bytes, Duration, Percentage, Count } from "../../util";
import { AxisUnits } from "../utils/domain";
import {
Bytes,
Duration,
Percentage,
Count,
FormatWithTimezone,
DATE_WITH_SECONDS_FORMAT_24_TZ,
} from "../../util";
import moment from "moment-timezone";

// Fallback color for series stroke if one is not defined.
const DEFAULT_STROKE = "#7e89a9";
Expand Down Expand Up @@ -96,7 +104,7 @@ function getFormattedValue(value: number, yAxisUnits: AxisUnits): string {
}

// Tooltip legend plugin for bar charts.
export function barTooltipPlugin(yAxis: AxisUnits): Plugin {
export function barTooltipPlugin(yAxis: AxisUnits, timezone: string): Plugin {
const cursorToolTip = {
tooltip: document.createElement("div"),
timeStamp: document.createElement("div"),
Expand All @@ -110,7 +118,11 @@ export function barTooltipPlugin(yAxis: AxisUnits): Plugin {
// get the current timestamp from the x axis and formatting as
// the Tooltip header.
const closestDataPointTimeMillis = u.data[0][u.posToIdx(left)];
timeStamp.textContent = formatTimeStamp(closestDataPointTimeMillis);
timeStamp.textContent = FormatWithTimezone(
moment(closestDataPointTimeMillis),
DATE_WITH_SECONDS_FORMAT_24_TZ,
timezone,
);

// Generating the series legend based on current state of µPlot
generateSeriesLegend(u, seriesLegend, yAxis);
Expand Down
Loading

0 comments on commit 0aeeafb

Please sign in to comment.