Skip to content

Commit

Permalink
ui: add manual refresh option to the Active Executions in SQL Activity
Browse files Browse the repository at this point in the history
Part of: #100663.

Previously, the Active Executions views in the SQL Activity pages only
only supported automatic refresh at a rate of every 10 seconds. This
is a papercut which may lead to some queries disappearing before a user
is able to investigate it. This commit adds adds support for manual
refresh in the Active Executions page as well as a toggle to switch
between automatic and manual refresh.

A summary of changes:

- A new component `RefreshControl` which contains an auto-refresh
  toggle, a manual refresh button, and a timestamp indicating when the
  last refresh was performed..
- `ActiveStatementsView` and `ActiveTransactionsView` make use of this
  component to control when their data gets refreshed.
- Analytics for the auto refresh toggle and the manual refresh button.

Release note (ui change): the active executions views in the SQL
Activity pages now support toggling between automatic and manual
refresh. A A manual refresh button is also added along with a timestamp
indicating when the last refresh was performed.
  • Loading branch information
gtr committed Jun 6, 2023
1 parent d660d51 commit f734b3e
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

export * from "./refreshControl";
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

@import "src/core/index.module";

.refresh-timestamp {
vertical-align: middle;
}

.refresh-icon {
margin-left: 12px;
margin-right: 6px;
vertical-align: middle;
}

.refresh-text {
color: $colors--primary-blue-3;
vertical-align: middle;
margin-right: 12px;
}

.ant-switch-checked {
background-color: $colors--primary-blue-3;
}

.refresh-button {
vertical-align: middle;
align-items: center;
}

.refresh-divider {
border-left: 1px solid $colors--neutral-4;
padding-left: 12px;
height: $line-height--large;
display: inline-flex;
align-items: center;
vertical-align: middle;
}

.auto-refresh-label {
padding-right: 8px;
vertical-align: middle;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { Switch } from "antd";
import "antd/lib/switch/style";
import React from "react";
import classNames from "classnames/bind";
import styles from "./refreshControl.module.scss";
import RefreshIcon from "src/icon/refreshIcon";
import { Timestamp } from "src/timestamp";
import { Moment } from "moment-timezone";
import { DATE_WITH_SECONDS_FORMAT_24_TZ, capitalize } from "src/util";
import { ExecutionType } from "../types";

const cx = classNames.bind(styles);

interface RefreshControlProps {
isAutoRefreshEnabled: boolean;
onToggleAutoRefresh: () => void;
onManualRefresh: () => void;
lastRefreshTimestamp: Moment;
execType: ExecutionType;
}

const REFRESH_BUTTON_COLOR = "#0055FF";

interface RefreshButtonProps {
onClick: () => void;
}

// RefreshButton consists of the RefreshIcon and the text "Refresh".
const RefreshButton: React.FC<RefreshButtonProps> = ({ onClick }) => (
<span className={cx("refresh-button")} onClick={onClick}>
<RefreshIcon color={REFRESH_BUTTON_COLOR} className={cx("refresh-icon")} />
<span className={cx("refresh-text")}>Refresh</span>
</span>
);

export const RefreshControl: React.FC<RefreshControlProps> = ({
isAutoRefreshEnabled,
onToggleAutoRefresh,
onManualRefresh,
lastRefreshTimestamp,
execType,
}) => {
return (
<div>
<span className={cx("refresh-timestamp")}>
<span>Active {capitalize(execType)} Executions As Of: </span>
{lastRefreshTimestamp && lastRefreshTimestamp.isValid() ? (
<Timestamp
time={lastRefreshTimestamp}
format={DATE_WITH_SECONDS_FORMAT_24_TZ}
/>
) : (
"N/A"
)}
</span>
<RefreshButton onClick={onManualRefresh} />
<span className={cx("refresh-divider")}>
<span className={cx("auto-refresh-label")}>Auto Refresh: </span>
<Switch
className={cx(`ant-switch-${isAutoRefreshEnabled ? "checked" : ""}`)}
checkedChildren={"On"}
unCheckedChildren={"Off"}
checked={isAutoRefreshEnabled}
onClick={onToggleAutoRefresh}
/>
</span>
</div>
);
};

export default RefreshControl;
45 changes: 45 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/icon/refreshIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import * as React from "react";

interface IconProps {
className?: string;
color?: string;
}

const RefreshIcon = ({ className, color }: IconProps) => (
<svg
className={className}
width="18"
height="18"
viewBox="0 0 24 24"
fill={color || "#394455"}
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_11029_22359)">
<path
d="M1.77601 11.9912C1.77601 8.58278 3.43606 5.47461 6.17336 3.58499V7.04636H7.93937V0H0.893012V1.766H5.78484C2.19985 3.92053 0.0100098 7.7351 0.0100098 11.9912C0.0100098 17.5364 3.93054 22.4636 9.33451 23.6821L9.72303 21.9514C5.11376 20.9095 1.77601 16.7064 1.77601 11.9735V11.9912Z"
fill={color || "#394455"}
/>
<path
d="M23.9924 11.9912C23.9924 6.35762 20.1601 1.53642 14.6678 0.300221L14.2793 2.03091C18.8886 3.07285 22.2263 7.27594 22.2263 12.0088C22.2263 15.4172 20.5663 18.5254 17.829 20.415V16.9536H16.063V24H23.1094V22.234H18.2175C21.8025 20.0795 23.9924 16.2649 23.9924 12.0088V11.9912Z"
fill={color || "#394455"}
/>
</g>
<defs>
<clipPath id="clip0_11029_22359">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);

export default RefreshIcon;
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ import {
ActiveStatementsViewStateProps,
AppState,
SortSetting,
analyticsActions,
} from "src";
import {
selectActiveStatements,
selectAppName,
selectExecutionStatus,
selectClusterLocksMaxApiSizeReached,
} from "src/selectors/activeExecutions.selectors";
import { actions as localStorageActions } from "src/store/localStorage";
import {
LocalStorageKeys,
actions as localStorageActions,
} from "src/store/localStorage";
import { actions as sessionsActions } from "src/store/sessions";
import { selectIsTenant } from "src/store/uiConfig";
import { localStorageSelector } from "../store/utils/selectors";
Expand All @@ -34,6 +38,12 @@ export const selectSortSetting = (state: AppState): SortSetting =>
export const selectFilters = (state: AppState): ActiveStatementFilters =>
localStorageSelector(state)["filters/ActiveStatementsPage"];

export const selectIsAutoRefreshEnabled = (state: AppState): boolean => {
return localStorageSelector(state)[
LocalStorageKeys.ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED
];
};

const selectLocalStorageColumns = (state: AppState) => {
const localStorage = localStorageSelector(state);
return localStorage["showColumns/ActiveStatementsPage"];
Expand All @@ -60,6 +70,8 @@ export const mapStateToActiveStatementsPageProps = (
internalAppNamePrefix: selectAppName(state),
isTenant: selectIsTenant(state),
maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state),
isAutoRefreshEnabled: selectIsAutoRefreshEnabled(state),
lastUpdated: state.adminUI?.sessions.lastUpdated,
});

export const mapDispatchToActiveStatementsPageProps = (
Expand Down Expand Up @@ -88,4 +100,27 @@ export const mapDispatchToActiveStatementsPageProps = (
value: ss,
}),
),
onAutoRefreshToggle: (isEnabled: boolean) => {
dispatch(
localStorageActions.update({
key: LocalStorageKeys.ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED,
value: isEnabled,
}),
);
dispatch(
analyticsActions.track({
name: "Auto Refresh Toggle",
page: "Statements",
value: isEnabled,
}),
);
},
onManualRefresh: () => {
dispatch(
analyticsActions.track({
name: "Manual Refresh",
page: "Statements",
}),
);
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import React, { useEffect, useState } from "react";
import classNames from "classnames/bind";
import { Moment } from "moment-timezone";
import { useHistory } from "react-router-dom";
import {
ISortedTablePagination,
Expand Down Expand Up @@ -39,6 +40,7 @@ import { Pagination } from "src/pagination";
import { InlineAlert } from "@cockroachlabs/ui-components";

import styles from "./statementsPage.module.scss";
import RefreshControl from "src/activeExecutions/refreshControl/refreshControl";

const cx = classNames.bind(styles);
const PAGE_SIZE = 20;
Expand All @@ -48,6 +50,8 @@ export type ActiveStatementsViewDispatchProps = {
onFiltersChange: (filters: ActiveStatementFilters) => void;
onSortChange: (ss: SortSetting) => void;
refreshLiveWorkload: () => void;
onAutoRefreshToggle: (isEnabled: boolean) => void;
onManualRefresh: () => void;
};

export type ActiveStatementsViewStateProps = {
Expand All @@ -60,6 +64,8 @@ export type ActiveStatementsViewStateProps = {
internalAppNamePrefix: string;
isTenant?: boolean;
maxSizeApiReached?: boolean;
isAutoRefreshEnabled?: boolean;
lastUpdated: Moment | null;
};

export type ActiveStatementsViewProps = ActiveStatementsViewStateProps &
Expand All @@ -79,6 +85,10 @@ export const ActiveStatementsView: React.FC<ActiveStatementsViewProps> = ({
internalAppNamePrefix,
isTenant,
maxSizeApiReached,
isAutoRefreshEnabled,
onAutoRefreshToggle,
lastUpdated,
onManualRefresh,
}: ActiveStatementsViewProps) => {
const [pagination, setPagination] = useState<ISortedTablePagination>({
current: 1,
Expand All @@ -90,13 +100,25 @@ export const ActiveStatementsView: React.FC<ActiveStatementsViewProps> = ({
);

useEffect(() => {
// Refresh every 10 seconds.
refreshLiveWorkload();
const interval = setInterval(refreshLiveWorkload, 10 * 1000);
return () => {
clearInterval(interval);
};
}, [refreshLiveWorkload]);
// useEffect hook which triggers an immediate data refresh if auto-refresh
// is enabled. It fetches the latest workload details by dispatching a
// refresh action when the component mounts, ensuring that users see fresh
// data as soon as they land on the page if auto-refresh is on.
if (isAutoRefreshEnabled) {
refreshLiveWorkload();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
// Refresh every 10 seconds if auto refresh is on.
if (isAutoRefreshEnabled) {
const interval = setInterval(refreshLiveWorkload, 10 * 1000);
return () => {
clearInterval(interval);
};
}
}, [isAutoRefreshEnabled, refreshLiveWorkload]);

useEffect(() => {
// We use this effect to sync settings defined on the URL (sort, filters),
Expand Down Expand Up @@ -160,6 +182,18 @@ export const ActiveStatementsView: React.FC<ActiveStatementsViewProps> = ({
resetPagination();
};

const onSubmitToggleAutoRefresh = () => {
// Refresh immediately when toggling auto-refresh on.
if (!isAutoRefreshEnabled) {
refreshLiveWorkload();
}
onAutoRefreshToggle(!isAutoRefreshEnabled);
};

const handleRefresh = () => {
onManualRefresh();
};

const clearSearch = () => onSubmitSearch("");
const clearFilters = () =>
onSubmitFilters({
Expand Down Expand Up @@ -204,6 +238,15 @@ export const ActiveStatementsView: React.FC<ActiveStatementsViewProps> = ({
filters={filters}
/>
</PageConfigItem>
<PageConfigItem>
<RefreshControl
isAutoRefreshEnabled={isAutoRefreshEnabled}
onToggleAutoRefresh={onSubmitToggleAutoRefresh}
onManualRefresh={handleRefresh}
lastRefreshTimestamp={lastUpdated}
execType={"statement"}
/>
</PageConfigItem>
</PageConfig>
<div className={cx("table-area")}>
<Loading
Expand Down
Loading

0 comments on commit f734b3e

Please sign in to comment.