diff --git a/pkg/cmd/roachtest/tests/restore.go b/pkg/cmd/roachtest/tests/restore.go index a7623d273468..072cfaab3b29 100644 --- a/pkg/cmd/roachtest/tests/restore.go +++ b/pkg/cmd/roachtest/tests/restore.go @@ -829,7 +829,7 @@ func (rd *restoreDriver) getAOST(ctx context.Context) { } func (rd *restoreDriver) restoreCmd(target, opts string) string { - return fmt.Sprintf(`./cockroach sql --insecure -e "RESTORE %s FROM %s IN %s AS OF SYSTEM TIME '%s' %s"`, + return fmt.Sprintf(`RESTORE %s FROM %s IN %s AS OF SYSTEM TIME '%s' %s`, target, rd.sp.backup.fullBackupDir, rd.sp.backup.backupCollection(), rd.aost, opts) } @@ -838,20 +838,25 @@ func (rd *restoreDriver) restoreCmd(target, opts string) string { // - "DATABASE tpce" will execute a database restore on the tpce cluster. // - "" will execute a cluster restore. func (rd *restoreDriver) run(ctx context.Context, target string) error { - return rd.c.RunE(ctx, rd.c.Node(1), rd.restoreCmd(target, "")) + conn, err := rd.c.ConnE(ctx, rd.t.L(), 1) + if err != nil { + return errors.Wrapf(err, "failed to connect to node 1; running restore") + } + _, err = conn.ExecContext(ctx, rd.restoreCmd(target, "")) + return err } func (rd *restoreDriver) runDetached( ctx context.Context, target string, node int, ) (jobspb.JobID, error) { - if err := rd.c.RunE(ctx, rd.c.Node(node), rd.restoreCmd(target, "WITH DETACHED")); err != nil { - return 0, err - } - db, err := rd.c.ConnE(ctx, rd.t.L(), rd.c.Node(node)[0]) if err != nil { return 0, errors.Wrapf(err, "failed to connect to node %d; running restore detached", node) } + if _, err = db.ExecContext(ctx, rd.restoreCmd(target, + "WITH DETACHED")); err != nil { + return 0, err + } var jobID jobspb.JobID if err := db.QueryRow(`SELECT job_id FROM [SHOW JOBS] WHERE job_type = 'RESTORE'`).Scan(&jobID); err != nil { return 0, err 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..8a8afb59df32 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/refreshControl.module.scss @@ -0,0 +1,49 @@ +// 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 { + 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; +} 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..0c4c71a77d48 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/activeExecutions/refreshControl/refreshControl.tsx @@ -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 { + onManualRefresh: () => void; +} + +// RefreshButton consists of the RefreshIcon and the text "Refresh". +const RefreshButton: React.FC = ({ onManualRefresh }) => ( + + + 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 e056fa46fd3e..656d7c2f3a9b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsPage.selectors.ts @@ -16,13 +16,17 @@ import { ActiveStatementsViewStateProps, AppState, SortSetting, + analyticsActions, } from "src"; import { selectActiveStatements, selectAppName, 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"; @@ -33,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"]; @@ -58,6 +68,8 @@ export const mapStateToActiveStatementsPageProps = ( internalAppNamePrefix: selectAppName(state), isTenant: selectIsTenant(state), maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state), + isAutoRefreshEnabled: selectIsAutoRefreshEnabled(state), + lastUpdated: state.adminUI?.sessions.lastUpdated, }); export const mapDispatchToActiveStatementsPageProps = ( @@ -86,4 +98,28 @@ 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(sessionsActions.refresh()); + dispatch( + analyticsActions.track({ + name: "Manual Refresh", + page: "Statements", + }), + ); + }, }); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsView.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsView.tsx index 1d89481bfd09..9b90e6933f9a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/activeStatementsView.tsx @@ -10,6 +10,7 @@ import React, { useEffect, useState } from "react"; import classNames from "classnames/bind"; +import moment, { Moment } from "moment-timezone"; import { useHistory } from "react-router-dom"; import { ISortedTablePagination, @@ -43,6 +44,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; @@ -52,6 +54,8 @@ export type ActiveStatementsViewDispatchProps = { onFiltersChange: (filters: ActiveStatementFilters) => void; onSortChange: (ss: SortSetting) => void; refreshLiveWorkload: () => void; + onAutoRefreshToggle: (isEnabled: boolean) => void; + onManualRefresh: () => void; }; export type ActiveStatementsViewStateProps = { @@ -63,6 +67,8 @@ export type ActiveStatementsViewStateProps = { internalAppNamePrefix: string; isTenant?: boolean; maxSizeApiReached?: boolean; + isAutoRefreshEnabled?: boolean; + lastUpdated: Moment | null; }; export type ActiveStatementsViewProps = ActiveStatementsViewStateProps & @@ -81,6 +87,10 @@ export const ActiveStatementsView: React.FC = ({ internalAppNamePrefix, isTenant, maxSizeApiReached, + isAutoRefreshEnabled, + onAutoRefreshToggle, + lastUpdated, + onManualRefresh, }: ActiveStatementsViewProps) => { const [pagination, setPagination] = useState({ current: 1, @@ -90,15 +100,59 @@ export const ActiveStatementsView: React.FC = ({ const [search, setSearch] = useState( queryByName(history.location, ACTIVE_STATEMENT_SEARCH_PARAM), ); + // Local state to store the difference between the current time and the last + // time the data was updated, in minutes. + const [minutesSinceLastRefresh, setMinutesSinceLastRefresh] = useState(0); + // Local state to store whether or not to display the refresh alert. + const [displayRefreshAlert, setDisplayRefreshAlert] = useState(false); useEffect(() => { - // Refresh every 10 seconds. - refreshLiveWorkload(); - const interval = setInterval(refreshLiveWorkload, 10 * 1000); + // 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 the workload every 10 seconds if auto refresh is on. + if (isAutoRefreshEnabled) { + const interval = setInterval(refreshLiveWorkload, 10 * 1000); + return () => { + clearInterval(interval); + }; + } + }, [isAutoRefreshEnabled, refreshLiveWorkload]); + + useEffect(() => { + // This useEffect hook checks the difference between the current time and + // the last time the data was updated. It triggers a state change to display + // an alert if the difference is greater than 10 minutes and auto-refresh + // is disabled. The check is performed immediately when the component mounts + // and then every 10 seconds thereafter. + const checkTimeDifference = () => { + if (!isAutoRefreshEnabled && lastUpdated) { + // Calculate the difference between the last updated time and the current time in minutes + const diffMinutes = moment().diff(lastUpdated, "minutes"); + if (diffMinutes >= 10) { + setDisplayRefreshAlert(true); + setMinutesSinceLastRefresh(diffMinutes); + } else { + setDisplayRefreshAlert(false); + } + } + }; + + checkTimeDifference(); + const intervalId = setInterval(checkTimeDifference, 10 * 1000); + return () => { - clearInterval(interval); + clearInterval(intervalId); }; - }, [refreshLiveWorkload]); + }, [lastUpdated, isAutoRefreshEnabled, setDisplayRefreshAlert]); useEffect(() => { // We use this effect to sync settings defined on the URL (sort, filters), @@ -162,6 +216,18 @@ export const ActiveStatementsView: React.FC = ({ resetPagination(); }; + const onSubmitToggleAutoRefresh = () => { + // Refresh immediately when toggling auto-refresh on. + if (!isAutoRefreshEnabled) { + refreshLiveWorkload(); + } + onAutoRefreshToggle(!isAutoRefreshEnabled); + }; + + const handleRefresh = () => { + onManualRefresh(); + }; + const clearSearch = () => onSubmitSearch(""); const clearFilters = () => onSubmitFilters({ @@ -209,7 +275,29 @@ export const ActiveStatementsView: React.FC = ({ filters={filters} /> + + + + {displayRefreshAlert && ( +
+ + Your active statements data is {minutesSinceLastRefresh} minutes + old. Consider refreshing for the latest information. + + } + /> +
+ )}
localStorageSelector(state)["sortSetting/ActiveTransactionsPage"]; @@ -58,6 +63,8 @@ export const mapStateToActiveTransactionsPageProps = ( internalAppNamePrefix: selectAppName(state), isTenant: selectIsTenant(state), maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state), + isAutoRefreshEnabled: selectIsAutoRefreshEnabled(state), + lastUpdated: state.adminUI?.sessions.lastUpdated, }); export const mapDispatchToActiveTransactionsPageProps = ( @@ -85,4 +92,28 @@ export const mapDispatchToActiveTransactionsPageProps = ( 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: "Transactions", + value: isEnabled, + }), + ); + }, + onManualRefresh: () => { + dispatch(sessionsActions.refresh()); + dispatch( + analyticsActions.track({ + name: "Manual Refresh", + page: "Transactions", + }), + ); + }, }); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/activeTransactionsView.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/activeTransactionsView.tsx index 33620388e097..a9d9b7da0322 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/activeTransactionsView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/activeTransactionsView.tsx @@ -41,6 +41,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, { Moment } from "moment-timezone"; + const cx = classNames.bind(styles); export type ActiveTransactionsViewDispatchProps = { @@ -48,6 +51,8 @@ export type ActiveTransactionsViewDispatchProps = { onFiltersChange: (filters: ActiveTransactionFilters) => void; onSortChange: (ss: SortSetting) => void; refreshLiveWorkload: () => void; + onAutoRefreshToggle: (isEnabled: boolean) => void; + onManualRefresh: () => void; }; export type ActiveTransactionsViewStateProps = { @@ -59,6 +64,8 @@ export type ActiveTransactionsViewStateProps = { internalAppNamePrefix: string; isTenant?: boolean; maxSizeApiReached?: boolean; + isAutoRefreshEnabled?: boolean; + lastUpdated: Moment | null; }; export type ActiveTransactionsViewProps = ActiveTransactionsViewStateProps & @@ -80,6 +87,10 @@ export const ActiveTransactionsView: React.FC = ({ filters, internalAppNamePrefix, maxSizeApiReached, + isAutoRefreshEnabled, + onAutoRefreshToggle, + lastUpdated, + onManualRefresh, }: ActiveTransactionsViewProps) => { const [pagination, setPagination] = useState({ current: 1, @@ -90,16 +101,59 @@ export const ActiveTransactionsView: React.FC = ({ const [search, setSearch] = useState( queryByName(history.location, RECENT_TXN_SEARCH_PARAM), ); + // Local state to store the difference between the current time and the last + // time the data was updated, in minutes. + const [minutesSinceLastRefresh, setMinutesSinceLastRefresh] = useState(0); + // Local state to store whether or not to display the refresh alert. + const [displayRefreshAlert, setDisplayRefreshAlert] = useState(false); 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); + }; + } + }, [isAutoRefreshEnabled, refreshLiveWorkload]); + + useEffect(() => { + // This useEffect hook checks the difference between the current time and + // the last time the data was updated. It triggers a state change to display + // an alert if the difference is greater than 10 minutes and auto-refresh + // is disabled. The check is performed immediately when the component mounts + // and then every 10 seconds thereafter. + const checkTimeDifference = () => { + if (!isAutoRefreshEnabled && lastUpdated) { + // Calculate the difference between the last updated time and the current time in minutes + const diffMinutes = moment().diff(lastUpdated, "minutes"); + if (diffMinutes >= 10) { + setDisplayRefreshAlert(true); + setMinutesSinceLastRefresh(diffMinutes); + } else { + setDisplayRefreshAlert(false); + } + } + }; + + checkTimeDifference(); + const intervalId = setInterval(checkTimeDifference, 10 * 1000); - const interval = setInterval(refreshLiveWorkload, 10 * 1000); return () => { - clearInterval(interval); + clearInterval(intervalId); }; - }, [refreshLiveWorkload]); + }, [lastUpdated, isAutoRefreshEnabled, setDisplayRefreshAlert]); useEffect(() => { // We use this effect to sync settings defined on the URL (sort, filters), @@ -161,6 +215,18 @@ export const ActiveTransactionsView: React.FC = ({ resetPagination(); }; + const onSubmitToggleAutoRefresh = () => { + // Refresh immediately when toggling auto-refresh on. + if (!isAutoRefreshEnabled) { + refreshLiveWorkload(); + } + onAutoRefreshToggle(!isAutoRefreshEnabled); + }; + + const handleRefresh = () => { + onManualRefresh(); + }; + const clearSearch = () => onSubmitSearch(""); const clearFilters = () => onSubmitFilters({ @@ -207,7 +273,29 @@ export const ActiveTransactionsView: React.FC = ({ filters={filters} /> + + + + {displayRefreshAlert && ( +
+ + Your active transactions data is {minutesSinceLastRefresh}{" "} + minutes old. Consider refreshing for the latest information. + + } + /> +
+ )}
({ sessionsError: state.cachedData?.sessions.lastError, internalAppNamePrefix: selectAppName(state), maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state), + isAutoRefreshEnabled: autoRefreshLocalSetting.selector(state), + lastUpdated: state.cachedData?.sessions.setAt, }); export const activeStatementsViewActions = { @@ -68,4 +71,7 @@ export const activeStatementsViewActions = { onFiltersChange: (filters: ActiveStatementFilters) => filtersLocalSetting.set(filters), onSortChange: (ss: SortSetting) => sortSettingLocalSetting.set(ss), + onAutoRefreshToggle: (isToggled: boolean) => + autoRefreshLocalSetting.set(isToggled), + onManualRefresh: () => refreshLiveWorkload(), }; 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 72c2814b2147..261e59ba1666 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/activeTransactionsSelectors.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/activeTransactionsSelectors.tsx @@ -23,6 +23,11 @@ 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"; +export const ACTIVE_EXECUTIONS_DISPLAY_REFRESH_ALERT = + "displayRefreshAlert/ActiveTransactionsPage"; + const transactionsColumnsLocalSetting = new LocalSetting< AdminUIState, string | null @@ -52,6 +57,14 @@ const sortSettingLocalSetting = new LocalSetting( { ascending: false, columnTitle: "startTime" }, ); +// autoRefreshLocalSetting is shared between the Active Statements and Active +// Transactions components. +export 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), @@ -60,6 +73,8 @@ export const mapStateToActiveTransactionsPageProps = (state: AdminUIState) => ({ sortSetting: sortSettingLocalSetting.selector(state), internalAppNamePrefix: selectAppName(state), maxSizeApiReached: selectClusterLocksMaxApiSizeReached(state), + isAutoRefreshEnabled: autoRefreshLocalSetting.selector(state), + lastUpdated: state.cachedData?.sessions.setAt, }); // This object is just for convenience so we don't need to supply dispatch to @@ -72,4 +87,7 @@ export const activeTransactionsPageActionCreators: ActiveTransactionsViewDispatc filtersLocalSetting.set(filters), onSortChange: (ss: SortSetting) => sortSettingLocalSetting.set(ss), refreshLiveWorkload, + onAutoRefreshToggle: (isToggled: boolean) => + autoRefreshLocalSetting.set(isToggled), + onManualRefresh: () => refreshLiveWorkload(), };