diff --git a/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/index.ts b/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/index.ts new file mode 100644 index 000000000000..9bfe2fc2d6a5 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/index.ts @@ -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"; diff --git a/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/refreshControl.module.scss b/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/refreshControl.module.scss new file mode 100644 index 000000000000..48c7d6d04fd2 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/refreshControl.module.scss @@ -0,0 +1,46 @@ +// 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: #0055FF; + vertical-align: middle; + margin-right: 12px; +} + +.refresh-button { + vertical-align: middle; + align-items: center; +} + +.refresh-divider { + border-left: 1px solid #c0c6d9; + 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; +} diff --git a/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/refreshControl.tsx b/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/refreshControl.tsx new file mode 100644 index 000000000000..560bc4c3d163 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/refreshControl.tsx @@ -0,0 +1,77 @@ +// 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_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 = ({ onClick }) => ( + + + Refresh + +); + +export const RefreshControl: React.FC = ({ + isAutoRefreshEnabled, + onToggleAutoRefresh, + onManualRefresh, + lastRefreshTimestamp, + execType, +}) => { + return ( +
+ + Active {capitalize(execType)} Executions As Of: + {lastRefreshTimestamp && lastRefreshTimestamp.isValid() ? ( + + ) : ( + "N/A" + )} + + + + Auto Refresh: + + +
+ ); +}; + +export default RefreshControl; diff --git a/pkg/ui/workspaces/cluster-ui/src/icon/refreshIcon.tsx b/pkg/ui/workspaces/cluster-ui/src/icon/refreshIcon.tsx new file mode 100644 index 000000000000..6ad5b1bb215e --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/icon/refreshIcon.tsx @@ -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) => ( + + + + + + + + + + + +); + +export default RefreshIcon; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsPage.selectors.ts index ef3f388ef435..6627489a8aa9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsPage.selectors.ts @@ -23,7 +23,10 @@ import { 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"; @@ -34,6 +37,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"]; @@ -60,6 +69,8 @@ export const mapStateToActiveStatementsPageProps = ( internalAppNamePrefix: selectAppName(state), isTenant: selectIsTenant(state), maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state), + isAutoRefreshEnabled: selectIsAutoRefreshEnabled(state), + lastUpdated: state.adminUI?.sessions.lastUpdated, }); export const mapDispatchToActiveStatementsPageProps = ( @@ -88,4 +99,12 @@ export const mapDispatchToActiveStatementsPageProps = ( value: ss, }), ), + onAutoRefreshToggle: (isEnabled: boolean) => { + dispatch( + localStorageActions.update({ + key: LocalStorageKeys.ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED, + value: isEnabled, + }), + ); + }, }); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsView.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsView.tsx index 4e421000e502..763d817ff8cf 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsView.tsx @@ -39,6 +39,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; @@ -48,6 +49,7 @@ export type ActiveStatementsViewDispatchProps = { onFiltersChange: (filters: ActiveStatementFilters) => void; onSortChange: (ss: SortSetting) => void; refreshLiveWorkload: () => void; + onAutoRefreshToggle: (isEnabled: boolean) => void; }; export type ActiveStatementsViewStateProps = { @@ -60,6 +62,8 @@ export type ActiveStatementsViewStateProps = { internalAppNamePrefix: string; isTenant?: boolean; maxSizeApiReached?: boolean; + isAutoRefreshEnabled?: boolean; + lastUpdated: Moment | null; }; export type ActiveStatementsViewProps = ActiveStatementsViewStateProps & @@ -79,6 +83,9 @@ export const ActiveStatementsView: React.FC = ({ internalAppNamePrefix, isTenant, maxSizeApiReached, + isAutoRefreshEnabled, + onAutoRefreshToggle, + lastUpdated, }: ActiveStatementsViewProps) => { const [pagination, setPagination] = useState({ current: 1, @@ -90,13 +97,25 @@ export const ActiveStatementsView: React.FC = ({ ); useEffect(() => { - // Refresh every 10 seconds. - 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); }; - }, [refreshLiveWorkload]); + } +}, [isAutoRefreshEnabled, refreshLiveWorkload]); useEffect(() => { // We use this effect to sync settings defined on the URL (sort, filters), @@ -160,6 +179,18 @@ export const ActiveStatementsView: React.FC = ({ resetPagination(); }; + const onSubmitToggleAutoRefresh = () => { + // Refresh immediately when toggling auto-refresh on. + if (!isAutoRefreshEnabled) { + refreshLiveWorkload(); + } + onAutoRefreshToggle(!isAutoRefreshEnabled); + }; + + const handleRefresh = () => { + refreshLiveWorkload(); + }; + const clearSearch = () => onSubmitSearch(""); const clearFilters = () => onSubmitFilters({ @@ -204,6 +235,15 @@ export const ActiveStatementsView: React.FC = ({ filters={filters} /> + + +
localStorageSelector(state)["sortSetting/ActiveTransactionsPage"]; @@ -60,6 +64,8 @@ export const mapStateToActiveTransactionsPageProps = ( internalAppNamePrefix: selectAppName(state), isTenant: selectIsTenant(state), maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state), + isAutoRefreshEnabled: selectIsAutoRefreshEnabled(state), + lastUpdated: state.adminUI?.sessions.lastUpdated, }); export const mapDispatchToActiveTransactionsPageProps = ( @@ -87,4 +93,12 @@ export const mapDispatchToActiveTransactionsPageProps = ( value: ss, }), ), + onAutoRefreshToggle: (isEnabled: boolean) => { + dispatch( + localStorageActions.update({ + key: LocalStorageKeys.ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED, + value: isEnabled, + }), + ); + }, }); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/activeTransactionsView.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/activeTransactionsView.tsx index 6944c33832fb..2120c4a92613 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/activeTransactionsView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/activeTransactionsView.tsx @@ -40,6 +40,9 @@ import { getTableSortFromURL } from "src/sortedtable/getTableSortFromURL"; import { getActiveTransactionFiltersFromURL } from "src/queryFilter/utils"; import { filterActiveTransactions } from "../activeExecutions/activeStatementUtils"; import { InlineAlert } from "@cockroachlabs/ui-components"; +import { RefreshControl } from "src/activeExecutions/refreshControl"; +import { Moment } from "moment-timezone"; + const cx = classNames.bind(styles); export type ActiveTransactionsViewDispatchProps = { @@ -47,6 +50,7 @@ export type ActiveTransactionsViewDispatchProps = { onFiltersChange: (filters: ActiveTransactionFilters) => void; onSortChange: (ss: SortSetting) => void; refreshLiveWorkload: () => void; + onAutoRefreshToggle: (isEnabled: boolean) => void; }; export type ActiveTransactionsViewStateProps = { @@ -59,6 +63,8 @@ export type ActiveTransactionsViewStateProps = { internalAppNamePrefix: string; isTenant?: boolean; maxSizeApiReached?: boolean; + isAutoRefreshEnabled?: boolean; + lastUpdated: Moment | null; }; export type ActiveTransactionsViewProps = ActiveTransactionsViewStateProps & @@ -81,6 +87,9 @@ export const ActiveTransactionsView: React.FC = ({ executionStatus, internalAppNamePrefix, maxSizeApiReached, + isAutoRefreshEnabled, + onAutoRefreshToggle, + lastUpdated, }: ActiveTransactionsViewProps) => { const [pagination, setPagination] = useState({ current: 1, @@ -93,14 +102,25 @@ export const ActiveTransactionsView: React.FC = ({ ); useEffect(() => { - // Refresh every 10 seconds. - 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); }; - }, [refreshLiveWorkload]); + } +}, [isAutoRefreshEnabled, refreshLiveWorkload]); useEffect(() => { // We use this effect to sync settings defined on the URL (sort, filters), @@ -162,6 +182,18 @@ export const ActiveTransactionsView: React.FC = ({ resetPagination(); }; + const onSubmitToggleAutoRefresh = () => { + // Refresh immediately when toggling auto-refresh on. + if (!isAutoRefreshEnabled) { + refreshLiveWorkload(); + } + onAutoRefreshToggle(!isAutoRefreshEnabled); + }; + + const handleRefresh = () => { + refreshLiveWorkload(); + }; + const clearSearch = () => onSubmitSearch(""); const clearFilters = () => onSubmitFilters({ @@ -207,6 +239,15 @@ export const ActiveTransactionsView: React.FC = ({ filters={filters} /> + + +
state.cachedData.sessions?.data; @@ -33,6 +34,10 @@ export const selectClusterLocksMaxApiSizeReached = ( return !!state.cachedData.clusterLocks?.data?.maxSizeReached; }; +export const selectIsAutoRefreshEnabled = (state: AdminUIState): boolean => { + return state.localSettings[ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED]; +}; + export const selectActiveExecutions = createSelector( selectSessions, selectClusterLocks, diff --git a/pkg/ui/workspaces/db-console/src/views/statements/activeStatementsSelectors.tsx b/pkg/ui/workspaces/db-console/src/views/statements/activeStatementsSelectors.tsx index 66fa9c1e5899..4e8f02a1dada 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/activeStatementsSelectors.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/activeStatementsSelectors.tsx @@ -18,10 +18,12 @@ import { selectAppName, selectExecutionStatus, selectClusterLocksMaxApiSizeReached, + selectIsAutoRefreshEnabled, } from "src/selectors"; import { refreshLiveWorkload } from "src/redux/apiReducers"; import { LocalSetting } from "src/redux/localsettings"; import { AdminUIState } from "src/redux/state"; +import { ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED } from "../transactions/activeTransactionsSelectors"; const selectedColumnsLocalSetting = new LocalSetting< AdminUIState, @@ -52,6 +54,14 @@ const sortSettingLocalSetting = new LocalSetting( { ascending: false, columnTitle: "startTime" }, ); +// autoRefreshLocalSetting is shared between the Active Statements and Active +// Transactions components. +const autoRefreshLocalSetting = new LocalSetting( + ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED, + (state: AdminUIState) => state.localSettings, + true, +); + export const mapStateToActiveStatementViewProps = (state: AdminUIState) => ({ filters: filtersLocalSetting.selector(state), selectedColumns: selectedColumnsLocalSetting.selectorToArray(state), @@ -61,6 +71,8 @@ export const mapStateToActiveStatementViewProps = (state: AdminUIState) => ({ sessionsError: state.cachedData?.sessions.lastError, internalAppNamePrefix: selectAppName(state), maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state), + isAutoRefreshEnabled: selectIsAutoRefreshEnabled(state), + lastUpdated: state.cachedData?.sessions.setAt, }); export const activeStatementsViewActions = { @@ -70,4 +82,6 @@ export const activeStatementsViewActions = { onFiltersChange: (filters: ActiveStatementFilters) => filtersLocalSetting.set(filters), onSortChange: (ss: SortSetting) => sortSettingLocalSetting.set(ss), + onAutoRefreshToggle: (isToggled: boolean) => + autoRefreshLocalSetting.set(isToggled), }; diff --git a/pkg/ui/workspaces/db-console/src/views/transactions/activeTransactionsSelectors.tsx b/pkg/ui/workspaces/db-console/src/views/transactions/activeTransactionsSelectors.tsx index 5abf2967b12e..1b31d87e7d9a 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/activeTransactionsSelectors.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/activeTransactionsSelectors.tsx @@ -19,11 +19,15 @@ import { selectActiveTransactions, selectExecutionStatus, selectClusterLocksMaxApiSizeReached, + selectIsAutoRefreshEnabled, } from "src/selectors"; import { refreshLiveWorkload } from "src/redux/apiReducers"; import { LocalSetting } from "src/redux/localsettings"; import { AdminUIState } from "src/redux/state"; +export const ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED = + "isAutoRefreshEnabled/ActiveExecutions"; + const transactionsColumnsLocalSetting = new LocalSetting< AdminUIState, string | null @@ -53,6 +57,14 @@ const sortSettingLocalSetting = new LocalSetting( { ascending: false, columnTitle: "startTime" }, ); +// autoRefreshLocalSetting is shared between the Active Statements and Active +// Transactions components. +const autoRefreshLocalSetting = new LocalSetting( + ACTIVE_EXECUTIONS_IS_AUTOREFRESH_ENABLED, + (state: AdminUIState) => state.localSettings, + true, +); + export const mapStateToActiveTransactionsPageProps = (state: AdminUIState) => ({ selectedColumns: transactionsColumnsLocalSetting.selectorToArray(state), transactions: selectActiveTransactions(state), @@ -62,6 +74,8 @@ export const mapStateToActiveTransactionsPageProps = (state: AdminUIState) => ({ sortSetting: sortSettingLocalSetting.selector(state), internalAppNamePrefix: selectAppName(state), maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state), + isAutoRefreshEnabled: selectIsAutoRefreshEnabled(state), + lastUpdated: state.cachedData?.sessions.setAt, }); // This object is just for convenience so we don't need to supply dispatch to @@ -74,4 +88,6 @@ export const activeTransactionsPageActionCreators: ActiveTransactionsViewDispatc filtersLocalSetting.set(filters), onSortChange: (ss: SortSetting) => sortSettingLocalSetting.set(ss), refreshLiveWorkload, + onAutoRefreshToggle: (isToggled: boolean) => + autoRefreshLocalSetting.set(isToggled), };