diff --git a/packages/ui/src/ui/containers/ClusterPage/ClusterPage.js b/packages/ui/src/ui/containers/ClusterPage/ClusterPage.js index f5b0ebd6b..a270ba013 100644 --- a/packages/ui/src/ui/containers/ClusterPage/ClusterPage.js +++ b/packages/ui/src/ui/containers/ClusterPage/ClusterPage.js @@ -37,7 +37,10 @@ import {setSetting} from '../../store/actions/settings'; import {unmountCluster, updateCluster} from '../../store/actions/cluster-params'; import {updateTitle} from '../../store/actions/global'; import {getClusterUiConfig} from '../../store/selectors/global'; -import {isQueryTrackerAllowed} from '../../store/selectors/global/experimental-pages'; +import { + isExperimentalPagesReady, + isQueryTrackerAllowed, +} from '../../store/selectors/global/experimental-pages'; import {getClusterConfig} from '../../utils'; import {NAMESPACES, SettingName} from '../../../shared/constants/settings'; import {getClusterPagePaneSizes, getStartingPage} from '../../store/selectors/settings'; @@ -88,6 +91,7 @@ class ClusterPage extends Component { allowChyt: PropTypes.bool, allowQueryTracker: PropTypes.bool, + allowStartPageRedirect: PropTypes.bool, }; state = { @@ -193,6 +197,7 @@ class ClusterPage extends Component { paramsError, allowChyt, allowQueryTracker, + allowStartPageRedirect, } = this.props; return isLoaded && !this.isParamsLoading() ? ( @@ -238,7 +243,9 @@ class ClusterPage extends Component { to={`/:cluster/${Page.COMPONENTS}/versions`} /> {makeExtraPageRoutes()} - + {allowStartPageRedirect && ( + + )} @@ -337,6 +344,7 @@ function mapStateToProps(state) { paramsCluster, allowQueryTracker: isQueryTrackerAllowed(state), allowChyt: Boolean(getClusterUiConfig(state).chyt_controller_base_url), + allowStartPageRedirect: isExperimentalPagesReady(state), }; } diff --git a/packages/ui/src/ui/hooks/use-updater.ts b/packages/ui/src/ui/hooks/use-updater.ts index b1bf640a6..927d90802 100644 --- a/packages/ui/src/ui/hooks/use-updater.ts +++ b/packages/ui/src/ui/hooks/use-updater.ts @@ -20,16 +20,27 @@ export type UseUpdaterOptions = { * if `true` then `fn()` will be called only once */ onlyOnce?: boolean; + /** + * Enforces to ignore 'Use auto refresh' user option + */ + forceAutoRefresh?: boolean; }; export function useUpdater( fn?: () => unknown, - {timeout = DEFAULT_UPDATER_TIMEOUT, destructFn, onlyOnce}: UseUpdaterOptions = {}, + { + timeout = DEFAULT_UPDATER_TIMEOUT, + destructFn, + onlyOnce, + forceAutoRefresh, + }: UseUpdaterOptions = {}, ) { const useAutoRefresh = useSelector(getUseAutoRefresh) as boolean; const optionsRef = React.useRef({skipNextCall: !useAutoRefresh}); - optionsRef.current.skipNextCall = !useAutoRefresh; + const allowAutoRefresh = forceAutoRefresh ?? useAutoRefresh; + + optionsRef.current.skipNextCall = !allowAutoRefresh; React.useEffect(() => { let updater: Updater | undefined; diff --git a/packages/ui/src/ui/pages/components/SwitchLeaderShortInfo/SwitchLeaderShortInfo.tsx b/packages/ui/src/ui/pages/components/SwitchLeaderShortInfo/SwitchLeaderShortInfo.tsx index 81883a5d5..2b8635f3d 100644 --- a/packages/ui/src/ui/pages/components/SwitchLeaderShortInfo/SwitchLeaderShortInfo.tsx +++ b/packages/ui/src/ui/pages/components/SwitchLeaderShortInfo/SwitchLeaderShortInfo.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; import cn from 'bem-cn-lite'; import MetaTable from '../../../components/MetaTable/MetaTable'; @@ -8,11 +8,12 @@ import {getStateForHost, loadMasters} from '../../../store/actions/system/master import {useDispatch} from 'react-redux'; import moment from 'moment'; import './SwitchLeaderShortInfo.scss'; +import {useUpdater} from '../../../hooks/use-updater'; const block = cn('switch-leader-short-info'); interface Props { - newLeaderAddress: string; + newLeaderPath: string; } export function SwitchLeaderShortInfo(props: Props) { @@ -21,44 +22,26 @@ export function SwitchLeaderShortInfo(props: Props) { const [finishTime, setFinishTime] = useState(); const dispatch = useDispatch(); - useEffect(() => { - const intervalId = setInterval(() => { + const updateCurrentTime = React.useCallback(() => { + if (!finishTime) { setCurrentTime(moment()); + } + }, [finishTime]); + useUpdater(updateCurrentTime, {timeout: 1000, forceAutoRefresh: true}); - if (finishTime) { - clearInterval(intervalId); - } - }, 1 * 1000); + const updateFn = React.useCallback(async () => { + if (finishTime) { + return; + } - return () => { - clearInterval(intervalId); - }; - }, []); + const hostState = await getStateForHost(props.newLeaderPath); - useEffect(() => { - let stillMounted = true; - - const waitForState = async () => { - try { - const hostState = await getStateForHost(props.newLeaderAddress); - - if (hostState === 'leading') { - setFinishTime(moment()); - dispatch(loadMasters()); - } - } catch { - if (stillMounted) { - waitForState(); - } - } - }; - - waitForState(); - - return () => { - stillMounted = false; - }; - }, [props.newLeaderAddress]); + if (hostState === 'leading') { + setFinishTime(moment()); + dispatch(loadMasters()); + } + }, [props.newLeaderPath, finishTime, dispatch]); + useUpdater(updateFn, {timeout: 3000, forceAutoRefresh: true}); return (
diff --git a/packages/ui/src/ui/pages/system/Masters/MasterGroup.js b/packages/ui/src/ui/pages/system/Masters/MasterGroup.js index 595929568..a3e4909e0 100644 --- a/packages/ui/src/ui/pages/system/Masters/MasterGroup.js +++ b/packages/ui/src/ui/pages/system/Masters/MasterGroup.js @@ -57,14 +57,6 @@ class MasterGroup extends Component { 'no-quorum': 'missing', unknown: 'unknown', }; - let leadingHost = ''; - const hosts = instances.map(({$address, state}) => { - if (state === 'leading') { - leadingHost = $address; - } - - return $address; - }); return ( @@ -96,13 +88,7 @@ class MasterGroup extends Component {
{cellTag && } {hammer.format['Hex'](cellTag)} - {cellId && ( - - )} + {cellId && }
diff --git a/packages/ui/src/ui/pages/system/Masters/SwitchLeader.tsx b/packages/ui/src/ui/pages/system/Masters/SwitchLeader.tsx index c9ccf8a5e..d10c31d4c 100644 --- a/packages/ui/src/ui/pages/system/Masters/SwitchLeader.tsx +++ b/packages/ui/src/ui/pages/system/Masters/SwitchLeader.tsx @@ -12,8 +12,7 @@ type SwitchLeaderDialogProps = { confirm: (newLeader: string) => Promise; visible: boolean; cellId: string; - hosts: string[]; - leadingHost: string; + hosts: Array<{getPath: () => string; state: 'leading'}>; }; type FormValues = { @@ -24,12 +23,16 @@ const SwitchLeaderDialog = (props: SwitchLeaderDialogProps) => { const [error, setError] = useState(undefined); const selectLeadingHostOptions = props.hosts.map((host) => { + const path = host.getPath(); return { - value: host, - content: host, + value: path, + content: path.split('/').pop(), }; }); + const leader = props.hosts.find(({state}) => state === 'leading'); + const leaderPath = leader?.getPath(); + return ( visible={props.visible} @@ -37,7 +40,7 @@ const SwitchLeaderDialog = (props: SwitchLeaderDialogProps) => { title: `Switch leader for ${props.cellId}`, }} initialValues={{ - leading_primary_master: [props.leadingHost], + leading_primary_master: leaderPath ? [leaderPath] : [], }} fields={[ { @@ -79,27 +82,22 @@ const SwitchLeaderDialog = (props: SwitchLeaderDialogProps) => { type SwitchLeaderButtonProps = { className: string; cellId: string; - hosts: string[]; - leadingHost: string; + hosts: Array<{getPath: () => string; state: 'leading'}>; }; -export const SwitchLeaderButton = ({ - cellId, - hosts, - leadingHost, - className, -}: SwitchLeaderButtonProps) => { +export const SwitchLeaderButton = ({cellId, hosts, className}: SwitchLeaderButtonProps) => { const [visible, setVisible] = useState(false); const handleClick = () => { setVisible(true); }; - const handleConfirm = async (newLeader: string) => { + const handleConfirm = async (newLeaderPath: string) => { + const leaderAddress = newLeaderPath.split('/').pop(); const switchLeader = () => { return ytApiV4Id.switchLeader(YTApiId.switchLeader, { cell_id: cellId, - new_leader_address: newLeader, + new_leader_address: leaderAddress, }); }; @@ -108,7 +106,7 @@ export const SwitchLeaderButton = ({ successContent() { return ( - + ); }, @@ -134,14 +132,15 @@ export const SwitchLeaderButton = ({ > - + {visible && ( + + )} ); }; diff --git a/packages/ui/src/ui/store/actions/global/experimental-pages.ts b/packages/ui/src/ui/store/actions/global/experimental-pages.ts index ab4f13bf5..9fc71dbc1 100644 --- a/packages/ui/src/ui/store/actions/global/experimental-pages.ts +++ b/packages/ui/src/ui/store/actions/global/experimental-pages.ts @@ -2,12 +2,18 @@ import {getCurrentUserName} from '../../../store/selectors/global'; import {GLOBAL_PARTIAL} from '../../../constants/global'; import UIFactory from '../../../UIFactory'; import {YTThunkAction} from '.'; +import {rumLogError} from '../../../rum/rum-counter'; export function loadAllowedExperimentalPages(): YTThunkAction { return (dispatch, getState) => { const login = getCurrentUserName(getState()); - return UIFactory.getAllowedExperimentalPages(login).then((allowedExperimentalPages) => { - dispatch({type: GLOBAL_PARTIAL, data: {allowedExperimentalPages}}); - }); + return UIFactory.getAllowedExperimentalPages(login) + .then((allowedExperimentalPages) => { + dispatch({type: GLOBAL_PARTIAL, data: {allowedExperimentalPages}}); + }) + .catch((error) => { + rumLogError({message: 'Failed to get experimental pages'}, error); + dispatch({type: GLOBAL_PARTIAL, data: {allowedExperimentalPages: []}}); + }); }; } diff --git a/packages/ui/src/ui/store/actions/system/masters.ts b/packages/ui/src/ui/store/actions/system/masters.ts index 4bae4e20e..692e13f22 100644 --- a/packages/ui/src/ui/store/actions/system/masters.ts +++ b/packages/ui/src/ui/store/actions/system/masters.ts @@ -180,6 +180,7 @@ async function loadMastersConfig(): Promise<[MastersConfigResponse, MasterAlert[ const primaryMaster = primaryMasterResult.output; const secondaryMasters = secondaryMastersResult.output; + const timestampProvierCellId = ypath.getValue(timestampProviderCellTag.output)?.cell_id; const timestampProviders = !timestampProvidersResult.output ? {} : { @@ -190,8 +191,10 @@ async function loadMastersConfig(): Promise<[MastersConfigResponse, MasterAlert[ attributes: ypath.getValue(value, '/@'), }; }), - cellId: ypath.getValue(timestampProviderCellTag.output)?.cell_id, - cellTag: getCellIdTag(ypath.getValue(timestampProviderCellTag.output)?.cell_id), + cellId: isSameClusterByCellId(masterCellId, timestampProvierCellId) + ? timestampProvierCellId + : undefined, + cellTag: getCellIdTag(timestampProvierCellId), }; const mainResult: MastersConfigResponse = { @@ -218,6 +221,7 @@ async function loadMastersConfig(): Promise<[MastersConfigResponse, MasterAlert[ attributes: ypath.getValue(value, '/@'), }; }), + cellId: replaceCellIdTag(masterCellId, Number(cellTag).toString(16)), cellTag: Number(cellTag), }; }), @@ -352,16 +356,13 @@ function loadHydra( } export const getStateForHost = async ( - host: string, + path: string, ): Promise<'leading' | 'following' | undefined> => { - const cypressPath = '//sys/primary_masters'; - const hydraPath = '/orchid/monitoring/hydra'; - const masterDataRequests: BatchSubRequest[] = [ { command: 'get' as const, parameters: { - path: cypressPath + '/' + host + hydraPath, + path: `${path}/orchid/monitoring/hydra`, ...USE_SUPRESS_SYNC, }, }, @@ -512,3 +513,26 @@ function getCellIdTag(uuid?: string): number | undefined { const [, , third = ''] = uuid.split('-'); return Number(`0x${third.substring(0, third.length - 4)}`); } + +function replaceCellIdTag(uuid?: string, newCellIdTag?: string) { + if (!uuid || !newCellIdTag) { + return undefined; + } + + const [first, second, third, ...rest] = uuid.split('-'); + const newThird = newCellIdTag + third.substring(third.length - 4); + return [first, second, newThird, ...rest].join('-'); +} + +function isSameClusterByCellId(lCell_id?: string, rClell_id?: string) { + if (!lCell_id || !rClell_id) { + return false; + } + + return removeCellTag(lCell_id) === removeCellTag(rClell_id); +} + +function removeCellTag(cellId: string) { + const [first, second, third, ...rest] = cellId.split('-'); + return [first, second, third.substring(third.length - 4), ...rest].join('-'); +} diff --git a/packages/ui/src/ui/store/reducers/global/index.js b/packages/ui/src/ui/store/reducers/global/index.js index 2b1bea80b..ba2480f41 100644 --- a/packages/ui/src/ui/store/reducers/global/index.js +++ b/packages/ui/src/ui/store/reducers/global/index.js @@ -80,7 +80,7 @@ const initialState = { asideHeaderWidth: 56, - allowedExperimentalPages: [], + allowedExperimentalPages: undefined, }; function updatedTitle(state, {cluster, page, path, clusters}) { diff --git a/packages/ui/src/ui/store/reducers/index.main.ts b/packages/ui/src/ui/store/reducers/index.main.ts index 65ef0ce01..cb659b602 100644 --- a/packages/ui/src/ui/store/reducers/index.main.ts +++ b/packages/ui/src/ui/store/reducers/index.main.ts @@ -98,7 +98,7 @@ export type RootState = Omit>, 'gl cluster?: string; rootPagesCluster?: string; asideHeaderWidth: number; - allowedExperimentalPages: Array; + allowedExperimentalPages?: Array; ytAuthCluster?: string; defaultPoolTree?: string; }; diff --git a/packages/ui/src/ui/store/reducers/system/masters.ts b/packages/ui/src/ui/store/reducers/system/masters.ts index 5f5ef0b1f..a4f9dc25a 100644 --- a/packages/ui/src/ui/store/reducers/system/masters.ts +++ b/packages/ui/src/ui/store/reducers/system/masters.ts @@ -255,6 +255,7 @@ function processMastersConfig( instances: map_(sortBy_(master.addresses, 'host'), (address) => { return new MasterInstance(address, 'secondary', master.cellTag); }), + cellId: master.cellId, cellTag: master.cellTag, }; }), @@ -262,6 +263,7 @@ function processMastersConfig( instances: map_(sortBy_(timestampProviders.addresses, 'host'), (address) => { return new MasterInstance(address, 'providers', timestampProviders.cellTag); }), + cellId: timestampProviders.cellId, cellTag: timestampProviders.cellTag, }, discovery: { @@ -352,6 +354,7 @@ function processMastersData( const providers = { instances: orderBy_(providersInstances, (instance) => instance.$address), cellTag: state.providers.cellTag, + cellId: state.providers.cellId, quorum: getQuorum(providersInstances), leader: getLeader(providersInstances), }; diff --git a/packages/ui/src/ui/store/selectors/global/experimental-pages.ts b/packages/ui/src/ui/store/selectors/global/experimental-pages.ts index af0dd0718..7e4efa9e0 100644 --- a/packages/ui/src/ui/store/selectors/global/experimental-pages.ts +++ b/packages/ui/src/ui/store/selectors/global/experimental-pages.ts @@ -11,10 +11,17 @@ export const getAllowedExperimentalPages = (state: RootState) => export const isQueryTrackerAllowed = createSelector( [isDeveloper, getAllowedExperimentalPages], - (isDeveloper, allowedPages) => { + (isDeveloper, allowedPages = []) => { const expPages = UIFactory.getExperimentalPages(); return ( isDeveloper || !expPages.includes(Page.QUERIES) || allowedPages.includes(Page.QUERIES) ); }, ); + +export const isExperimentalPagesReady = (state: RootState) => { + return ( + UIFactory.getExperimentalPages().length == 0 || + getAllowedExperimentalPages(state) !== undefined + ); +}; diff --git a/packages/ui/src/ui/store/selectors/slideoutMenu.ts b/packages/ui/src/ui/store/selectors/slideoutMenu.ts index 89b02b5d0..a7251ce0c 100644 --- a/packages/ui/src/ui/store/selectors/slideoutMenu.ts +++ b/packages/ui/src/ui/store/selectors/slideoutMenu.ts @@ -48,7 +48,7 @@ const getRecentPagesInfoRaw = createSelector( const expPages = UIFactory.getExperimentalPages(); const hiddenPages = new Set( expPages.filter((expPages) => { - return !allowExpPages.includes(expPages); + return !allowExpPages?.includes(expPages); }), ); diff --git a/packages/ui/tests/screenshots/pages/components/components.base.screen.ts b/packages/ui/tests/screenshots/pages/components/components.base.screen.ts index 8602bf05f..3f5ace9cd 100644 --- a/packages/ui/tests/screenshots/pages/components/components.base.screen.ts +++ b/packages/ui/tests/screenshots/pages/components/components.base.screen.ts @@ -112,10 +112,10 @@ test('Components - Versions', async ({page}) => { await page.waitForSelector('.versions-summary'); await replaceInnerHtml(page, { - '.versions-summary__version span': 'XX.X.X-local-os~XXXXXXXXXXXXXXXX+distbuild', '.yt-host__tooltip': 'local:XXX', + '.version-cell__text': 'XX.X.X-local-os~XXXXXXXXXXXXXXXX+distbuild', }); - replaceInnerHtmlForDateTime(page, ['.components-versions__table-item_type_start-time span']); + replaceInnerHtmlForDateTime(page, ['td.components-versions__table-item_type_start-time span']); await expect(page).toHaveScreenshot(); }); diff --git a/packages/ui/tests/screenshots/pages/components/components.base.screen.ts-snapshots/Components---Versions-1-chromium-linux.png b/packages/ui/tests/screenshots/pages/components/components.base.screen.ts-snapshots/Components---Versions-1-chromium-linux.png index 7389ddb97..77ead3296 100644 Binary files a/packages/ui/tests/screenshots/pages/components/components.base.screen.ts-snapshots/Components---Versions-1-chromium-linux.png and b/packages/ui/tests/screenshots/pages/components/components.base.screen.ts-snapshots/Components---Versions-1-chromium-linux.png differ