Skip to content

Commit

Permalink
feat(System): introduce new switch leader button
Browse files Browse the repository at this point in the history
  • Loading branch information
vrozaev committed May 21, 2024
1 parent c80d4f9 commit 43f5034
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/ui/src/shared/constants/yt-api-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,5 @@ export enum YTApiId {
removeMaintenance,
maintenanceRequests,
getQueryTrackerInfo,
switchLeader,
}
3 changes: 2 additions & 1 deletion packages/ui/src/ui/components/Icon/importGravityIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CircleXmark from '@gravity-ui/icons/svgs/circle-xmark.svg';
import FolderArrowDown from '@gravity-ui/icons/svgs/folder-arrow-down.svg';
import FolderOpen from '@gravity-ui/icons/svgs/folder-open.svg';
import LayoutSideContent from '@gravity-ui/icons/svgs/layout-side-content.svg';
import {AbbrSql} from '@gravity-ui/icons';
import {AbbrSql, CrownDiamond} from '@gravity-ui/icons';

export const iconNames = {
['lock']: Lock,
Expand All @@ -14,4 +14,5 @@ export const iconNames = {
['folder-open']: FolderOpen,
['layout-side-content']: LayoutSideContent,
['sql']: AbbrSql,
['crowndiamond']: CrownDiamond,
};
3 changes: 1 addition & 2 deletions packages/ui/src/ui/constants/accounts/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {createPrefix} from './../utils';
import createActionTypes from '../../constants/utils';
import createActionTypes, {createPrefix} from './../utils';
import type {ValueOf} from '../../types';

const ACCOUNTS_PREFIX = createPrefix('ACCOUNTS');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.switch-leader-short-info {
&__state {
text-transform: capitalize;

&_state_complete {
color: var(--success-color);
}
}

&__value {
text-transform: capitalize;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, {useEffect, useRef, useState} from 'react';
import cn from 'bem-cn-lite';

import MetaTable from '../../../components/MetaTable/MetaTable';
// @ts-ignore
import format from '../../../common/hammer/format';
import {getStateForHost, loadMasters} from '../../../store/actions/system/masters';
import {useDispatch} from 'react-redux';
import moment from 'moment';
import './SwitchLeaderShortInfo.scss';

const block = cn('switch-leader-short-info');

interface Props {
newLeaderAddress: string;
}

export function SwitchLeaderShortInfo(props: Props) {
const startTime = useRef(moment.now());
const [currentTime, setCurrentTime] = useState<any>(moment());
const [finishTime, setFinishTime] = useState<any>();
const dispatch = useDispatch();

useEffect(() => {
const intervalId = setInterval(() => {
setCurrentTime(moment());

if (finishTime) {
clearInterval(intervalId);
}
}, 1 * 1000);

return () => {
clearInterval(intervalId);
};
}, []);

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]);

return (
<div className={block()}>
<MetaTable
items={[
{
key: 'Duration',
value: format.TimeDuration(
(finishTime || currentTime).diff(startTime.current),
),
},
{
key: 'Status',
value: (
<SwitchLeaderShortInfoStatus
state={finishTime ? 'complete' : 'in progress'}
/>
),
},
]}
/>
</div>
);
}

function SwitchLeaderShortInfoStatus({state}: {state: string}) {
return <span className={block('state', {state})}>{state}</span>;
}
20 changes: 18 additions & 2 deletions packages/ui/src/ui/pages/system/Masters/MasterGroup.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, {Component, Fragment} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import _ from 'lodash';
import block from 'bem-cn-lite';
import {Text} from '@gravity-ui/uikit';

Expand All @@ -14,6 +13,8 @@ import ypath from '../../../common/thor/ypath';
import Icon from '../../../components/Icon/Icon';
import {Tooltip} from '../../../components/Tooltip/Tooltip';
import NodeQuad from '../NodeQuad/NodeQuad';
import {SwitchLeaderButton} from './SwitchLeader';
import _ from 'lodash';

import './MasterGroup.scss';

Expand Down Expand Up @@ -134,7 +135,7 @@ class MasterGroup extends Component {
};

renderQuorum() {
const {quorum, cellTag} = this.props;
const {quorum, cellTag, cellId, instances} = this.props;
const status = quorum ? quorum.status : 'unknown';
const quorumTitle = quorum && `Leader committed version: ${quorum.leaderCommitedVersion}`;
const cellTitle = `Cell tag: ${cellTag}`;
Expand All @@ -150,6 +151,14 @@ 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 (
<Fragment>
Expand Down Expand Up @@ -181,6 +190,13 @@ class MasterGroup extends Component {
<div className={b('quorum-cell')} title={cellTitle}>
{cellTag && <Icon className={b('icon-glyph')} face="solid" awesome="tag" />}
{hammer.format['Hex'](cellTag)}
{cellId && (
<SwitchLeaderButton
cellId={cellId}
hosts={hosts}
leadingHost={leadingHost}
/>
)}
</div>
</div>
</Fragment>
Expand Down
147 changes: 147 additions & 0 deletions packages/ui/src/ui/pages/system/Masters/SwitchLeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, {useState} from 'react';
import Icon from '../../../components/Icon/Icon';
import Button from '../../../components/Button/Button';
import {YTApiId, ytApiV4Id} from '../../../rum/rum-wrap-api';
import {wrapApiPromiseByToaster} from '../../../utils/utils';
import {YTDFDialog, makeErrorFields} from '../../../components/Dialog/Dialog';
import {SwitchLeaderShortInfo} from '../../../pages/components/SwitchLeaderShortInfo/SwitchLeaderShortInfo';
import {AppStoreProvider} from '../../../containers/App/AppStoreProvider';

type SwitchLeaderDialogProps = {
cancel: () => void;
confirm: (newLeader: string) => Promise<void>;
visible: boolean;
cellId: string;
hosts: string[];
leadingHost: string;
};

type FormValues = {
leading_primary_master: string[];
};

const SwitchLeaderDialog = (props: SwitchLeaderDialogProps) => {
const [error, setError] = useState(undefined);

const selectLeadingHostOptions = props.hosts.map((host) => {
return {
value: host,
content: host,
};
});

return (
<YTDFDialog<FormValues>
visible={props.visible}
headerProps={{
title: `Switch leader for ${props.cellId}`,
}}
initialValues={{
leading_primary_master: [props.leadingHost],
}}
fields={[
{
type: 'select',
caption: ' Leading primary master',
name: 'leading_primary_master',
required: true,
extras: {
options: selectLeadingHostOptions,
placeholder: 'New leading primary master',
width: 'max',
filterable: true,
},
},
...makeErrorFields([error]),
]}
footerProps={{
textApply: 'Switch leader',
}}
onAdd={(form) => {
const {leading_primary_master} = form.getState().values;

return props
.confirm(leading_primary_master[0])
.then(() => {
setError(undefined);
})
.catch((e) => {
setError(e);
throw e;
});
}}
onClose={props.cancel}
pristineSubmittable={true}
/>
);
};

type SwitchLeaderButtonProps = {
className: string;
cellId: string;
hosts: string[];
leadingHost: string;
};

export const SwitchLeaderButton = ({
cellId,
hosts,
leadingHost,
className,
}: SwitchLeaderButtonProps) => {
const [visible, setVisible] = useState(false);

const handleClick = () => {
setVisible(true);
};

const handleConfirm = async (newLeader: string) => {
const switchLeader = () => {
return ytApiV4Id.switchLeader(YTApiId.switchLeader, {
cell_id: cellId,
new_leader_address: newLeader,
});
};

wrapApiPromiseByToaster(switchLeader(), {
toasterName: 'switch-leader',
successContent() {
return (
<AppStoreProvider>
<SwitchLeaderShortInfo newLeaderAddress={newLeader} />
</AppStoreProvider>
);
},
successTitle: 'Leader switch initiated',
autoHide: false,
});

setVisible(false);
};

const handleCancel = () => {
setVisible(false);
};

return (
<React.Fragment>
<Button
className={className}
view="flat-secondary"
onClick={handleClick}
withTooltip
tooltipProps={{content: 'Switch leader'}}
>
<Icon awesome="crowndiamond" />
</Button>
<SwitchLeaderDialog
cellId={cellId}
hosts={hosts}
leadingHost={leadingHost}
confirm={handleConfirm}
cancel={handleCancel}
visible={visible}
/>
</React.Fragment>
);
};
4 changes: 4 additions & 0 deletions packages/ui/src/ui/rum/rum-wrap-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ type YTApiV4WithId = {
access_control_objects: string[];
supported_features: {access_control: boolean};
}>;
switchLeader(
id: YTApiId,
...args: ApiMethodParameters<{cell_id: string; new_leader_address: string}>
): Promise<any>;
[method: string]: (id: YTApiId, ...args: ApiMethodParameters<any>) => Promise<any>;
};

Expand Down
Loading

0 comments on commit 43f5034

Please sign in to comment.