diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Base/Tooltip/index.jsx b/src/components/Base/Tooltip/index.jsx index 9703ed9c..eeb78917 100644 --- a/src/components/Base/Tooltip/index.jsx +++ b/src/components/Base/Tooltip/index.jsx @@ -43,7 +43,10 @@ export default class Tooltip extends React.Component { this.setState({ visible: false }, this.props.onVisibleChange(false, this.target)); }; - handleTogglePopper = () => { + handleTogglePopper = e => { + if (e.stopPropagation) { + e.stopPropagation(); + } this.state.visible ? this.hidePopper() : this.showPopper(); }; diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index aa028cfc..2dd1c8fd 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -40,6 +40,9 @@ class Header extends Component {
  • {t('Dashboard')}
  • +
  • + {t('Profile')} +
  • {t('Log out')}
  • diff --git a/src/components/Layout/Card/index.jsx b/src/components/Layout/Card/index.jsx index a2586d4b..43fd4dd3 100644 --- a/src/components/Layout/Card/index.jsx +++ b/src/components/Layout/Card/index.jsx @@ -6,8 +6,10 @@ import Panel from '../Panel'; import styles from './index.scss'; -const Card = ({ className, children }) => ( - {children} +const Card = ({ className, children, ...others }) => ( + + {children} + ); Card.propTypes = { diff --git a/src/components/Layout/index.jsx b/src/components/Layout/index.jsx index 2e8173c2..935be3f9 100644 --- a/src/components/Layout/index.jsx +++ b/src/components/Layout/index.jsx @@ -22,7 +22,8 @@ export default class Layout extends React.Component { isLoading: PropTypes.bool, loadClass: PropTypes.string, sockMessage: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - listenToJob: PropTypes.func + listenToJob: PropTypes.func, + isProfile: PropTypes.bool }; static defaultProps = { @@ -31,7 +32,8 @@ export default class Layout extends React.Component { noNotification: false, backBtn: null, sockMessage: '', - listenToJob: noop + listenToJob: noop, + isProfile: false }; constructor(props) { @@ -48,11 +50,13 @@ export default class Layout extends React.Component { } } - renderTabs() { + renderTabs(isProfile) { const loginRole = getSessInfo('role', this.props.sessInfo); const normalLinks = [{ '': 'overview' }, 'apps', 'clusters', 'runtimes']; - - if (loginRole === 'normal') { + if (isProfile) { + this.linkPrefix = '/profile'; + this.availableLinks = [{ '': 'profile' }, { sshkeys: 'SSH Keys' }]; + } else if (loginRole === 'normal') { this.availableLinks = [...normalLinks]; this.availableLinks.splice(1, 1); } else if (loginRole === 'developer') { @@ -90,12 +94,13 @@ export default class Layout extends React.Component { children, isLoading, loadClass, - backBtn + backBtn, + isProfile } = this.props; return (
    - {noTabs ? null : this.renderTabs()} + {noTabs ? null : this.renderTabs(isProfile)} {noNotification ? null : } {backBtn} {this.renderSocketMessage()} diff --git a/src/components/TabsNav/index.jsx b/src/components/TabsNav/index.jsx index ae99c34e..83bdf762 100644 --- a/src/components/TabsNav/index.jsx +++ b/src/components/TabsNav/index.jsx @@ -25,8 +25,12 @@ const normalizeLink = (link, prefix = '') => { const isLinkActive = (curLink, match, location) => { const { pathname } = location; + if (curLink === '/dashboard') { return curLink === pathname; + } + if (curLink === '/profile') { + return curLink === pathname; } else { let try_match = pathname.indexOf(curLink) === 0; if (!try_match) { @@ -38,22 +42,28 @@ const isLinkActive = (curLink, match, location) => { } }; -const LinkItem = ({ link, label }) => ( - - {t => ( -
  • - - {t(capitalize(label))} - -
  • - )} -
    -); +const LinkItem = ({ link, label }) => { + if (link.indexOf('/profile/sshkeys') === -1) { + label = capitalize(label); + } + + return ( + + {t => ( +
  • + + {t(label)} + +
  • + )} +
    + ); +}; LinkItem.propTypes = { link: PropTypes.string.isRequired, diff --git a/src/pages/Profile/SSHKeys/index.jsx b/src/pages/Profile/SSHKeys/index.jsx new file mode 100644 index 00000000..b0ed08b3 --- /dev/null +++ b/src/pages/Profile/SSHKeys/index.jsx @@ -0,0 +1,300 @@ +import React, { Component, Fragment } from 'react'; +import { observer, inject } from 'mobx-react'; +import { Link } from 'react-router-dom'; +import classNames from 'classnames'; +import { translate } from 'react-i18next'; + +import { Table, Popover, Radio, Button, Input, Select, Icon } from 'components/Base'; +import Layout, { CreateResource, Dialog, Panel, Grid, Row, Section, Card } from 'components/Layout'; +import Toolbar from 'components/Toolbar'; +import Status from 'components/Status'; +import TdName from 'components/TdName'; +import Configuration from 'pages/Admin/Clusters/Detail/Configuration'; +import { getObjName, formatTime } from 'utils'; +import styles from './index.scss'; + +@translate() +@inject(({ rootStore }) => ({ + userStore: rootStore.userStore, + clusterStore: rootStore.clusterStore +})) +@observer +export default class SSHKeys extends Component { + static async onEnter({ clusterStore }) { + await clusterStore.fetchKeyPairs(); + await clusterStore.fetchAll({ + limit: 200, + active: ['active', 'stopped', 'ceased', 'pending', 'suspended', 'deleted'] + }); + } + + constructor(props) { + super(props); + const { clusterStore } = this.props; + clusterStore.currentPairId = ''; + clusterStore.loadNodeInit(); + } + + onClickPair = item => { + const { clusterStore } = this.props; + clusterStore.currentPairId = item.key_pair_id; + clusterStore.fetchNodes({ node_id: item.node_id }); + }; + + showDeleteModal = (e, pairId) => { + e.stopPropagation(); + const { clusterStore } = this.props; + clusterStore.pairId = pairId; + clusterStore.showModal('delPair'); + }; + + renderOperateMenu = pairId => { + const { t } = this.props; + return ( +
    + this.showDeleteModal(e, pairId)}>{t('Delete SSH Key')} +
    + ); + }; + + renderDeleteModal = () => { + const { clusterStore, t } = this.props; + const { isModalOpen, removeKeyPairs, hideModal } = clusterStore; + + return ( + + Are you sure delete this SSH Key? + + ); + }; + + renderForm() { + const { t } = this.props; + + return ( +
    +
    + + +

    + {t('The name of the SSH key')} +

    +
    +
    + + + Create a new keypair + Use the existing public key + +
    +
    + + +
    +
    + + + + +
    +
    + ); + } + + renderAside() { + return ( +

    + SSH keys allow you to establish a secure connection between your computer and Cluster Nodes. +

    + ); + } + + renderCard(pair) { + const { t } = this.props; + + return ( +
    +
    {pair.name}
    +
    {pair.pub_key}
    +
    + Created on: {formatTime(pair.create_time, 'YYYY/MM/DD HH:mm:ss')} +
    +
    + ); + } + + renderDetail() { + const { clusterStore, t } = this.props; + + const { + isLoading, + searchNode, + onSearchNode, + onClearNode, + onRefreshNode, + onChangeNodeStatus, + selectNodeStatus, + totalNodeCount, + clusters, + keyPairs, + currentPairId + } = clusterStore; + + const clusterNodes = clusterStore.clusterNodes.toJSON(); + const columns = [ + { + title: t('Name'), + key: 'name', + width: '155px', + render: item => + }, + { + title: t('Cluster Name'), + key: 'cluster_id', + render: item => ( + + ) + }, + { + title: t('Status'), + key: 'status', + width: '102px', + // fixme: prop type check case sensitive + render: item => + }, + { + title: t('Role'), + key: 'role', + dataIndex: 'role' + }, + { + title: t('Private IP'), + key: 'private_ip', + dataIndex: 'private_ip', + width: '100px' + }, + { + title: t('Configuration'), + key: 'configuration', + width: '100px', + render: item => + }, + { + title: t('Actions'), + key: 'actions', + width: '84px', + render: item => ( +
    + + + +
    + ) + } + ]; + const filterList = [ + { + key: 'status', + conditions: [ + { name: t('Pending'), value: 'pending' }, + { name: t('Active'), value: 'active' }, + { name: t('Stopped'), value: 'stopped' }, + { name: t('Suspended'), value: 'suspended' }, + { name: t('Deleted'), value: 'deleted' }, + { name: t('Ceased'), value: 'ceased' } + ], + onChangeFilter: onChangeNodeStatus, + selectValue: selectNodeStatus + } + ]; + const pagination = { + tableType: 'Clusters', + onChange: () => {}, + total: totalNodeCount, + current: 1 + }; + + return ( + +
    SSH Keys ({keyPairs.length})
    + +
    + {keyPairs.map(pair => ( + this.onClickPair(pair)} + className={classNames(styles.sshCardOuter, { + [styles.active]: pair.key_pair_id === currentPairId + })} + > + {this.renderCard(pair)} + + + + + ))} + +
    + +
    + + + + + + + + + + ); + } + + render() { + const { keyPairs } = this.props.clusterStore; + + return ( + + {keyPairs.length > 0 ? ( + this.renderDetail() + ) : ( + + {this.renderForm()} + + )} + {this.renderDeleteModal()} + + ); + } +} diff --git a/src/pages/Profile/SSHKeys/index.scss b/src/pages/Profile/SSHKeys/index.scss new file mode 100644 index 00000000..d947d5cc --- /dev/null +++ b/src/pages/Profile/SSHKeys/index.scss @@ -0,0 +1,95 @@ +@import '~scss/vars'; + +form.createForm { + display: block; + //padding: 36px 0 0 36px; + + &>div{ + margin-bottom: 24px; + } + .name{ + display: inline-block; + margin-right: 24px; + width: 168px; + line-height: 16px; + font-size: 14px; + font-weight: 500; + color: $N500; + text-align: right; + } + .selectName{ + float: left; + line-height: 32px; + } + .input{ + width: 256px; + } + .radio,.checkbox{ + margin-right: 30px; + } + .select{ + width: 256px; + margin-right: 12px; + } + .rightShow{ + margin-top: 8px; + margin-left: 192px; + font-size: 12px; + line-height: 1.67; + color: $N100; + } + .submitBtnGroup{ + margin: 0 -32px; + padding: 16px 0 16px 224px; + border-top: 1px solid $N10; + margin-bottom: 0; + button{ + margin-right: 12px; + } + } +} + +.sshTitle{ + margin: 0 auto 20px; + width: $content-width; + font-size: 24px; + font-weight: 500; + line-height: 28px; + color: $N500; +} +.sshCard { + .title { + margin-bottom: 18px; + line-height: 16px; + font-size: 14px; + font-weight: 500; + color: $N500; + max-width: 230px; + @include textCut; + } + .item { + margin-bottom: 12px; + line-height: 20px; + font-size: 12px; + color: $N75; + word-break: break-all; + word-wrap: break-word; + @include textCut; + } +} +.sshCardOuter { + border: 1px solid transparent; + cursor: pointer; + &.active{ + border: 1px solid $P75; + } + :global{ + .operation{ + top: 16px; + right: 16px; + } + } +} +.addButton { + width: 100%; +} diff --git a/src/pages/Profile/index.jsx b/src/pages/Profile/index.jsx new file mode 100644 index 00000000..20c1872c --- /dev/null +++ b/src/pages/Profile/index.jsx @@ -0,0 +1,19 @@ +import React, { Component } from 'react'; +import { observer, inject } from 'mobx-react'; + +import Layout, { Dialog, Grid, Row, Section, Card } from 'components/Layout'; +import styles from './index.scss'; + +@inject(({ rootStore }) => ({ + userStore: rootStore.userStore +})) +@observer +export default class Profile extends Component { + render() { + return ( + + Develop ... + + ); + } +} diff --git a/src/pages/Profile/index.scss b/src/pages/Profile/index.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/routes/index.js b/src/routes/index.js index 2c5490d5..cfe1034f 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -2,6 +2,8 @@ import Home from 'pages/Home'; import Login from 'pages/Login'; import AppDetail from 'pages/AppDetail'; import * as Dash from 'pages/Admin'; +import Profile from 'pages/Profile'; +import SSHKeys from 'pages/Profile/SSHKeys'; const useExactRoute = true; const dashboardPrefix = '/dashboard'; @@ -43,6 +45,9 @@ const routes = { '/:dash/categories': Dash.Categories, '/:dash/category/:categoryId': Dash.CategoryDetail, + '/profile': Profile, + '/profile/sshkeys': SSHKeys, + '*': Home }; diff --git a/src/stores/cluster/index.js b/src/stores/cluster/index.js index 8e25bc81..29221fae 100644 --- a/src/stores/cluster/index.js +++ b/src/stores/cluster/index.js @@ -14,6 +14,7 @@ export default class ClusterStore extends Store { @observable isLoading = false; @observable totalCount = 0; @observable clusterCount = 0; + @observable totalNodeCount = 0; @observable isModalOpen = false; @observable clusterId; // current delete cluster_id @@ -34,6 +35,10 @@ export default class ClusterStore extends Store { @observable selectStatus = ''; @observable defaultStatus = ['active', 'stopped', 'ceased', 'pending', 'suspended']; + @observable keyPairs = []; + @observable pairId = ''; + @observable currentPairId = ''; + @action.bound showModal = type => { this.modalType = type; @@ -114,11 +119,12 @@ export default class ClusterStore extends Store { if (this.searchNode) { params.search_word = this.searchNode; } - if (!params.selectNodeStatus) { - params.status = this.selectNodeStatus ? this.selectNodeStatus : this.cluster.status; + if (!params.status) { + params.status = this.selectNodeStatus ? this.selectNodeStatus : this.defaultStatus; } const result = await this.request.get(`clusters/nodes`, assign(defaultParams, params)); this.clusterNodes = get(result, 'cluster_node_set', []); + this.totalNodeCount = get(result, 'total_count', 0); this.isLoading = false; }; @@ -318,4 +324,50 @@ export default class ClusterStore extends Store { this.selectNodeStatus = ''; this.searchNode = ''; }; + + @action + fetchKeyPairs = async (params = {}) => { + let defaultParams = { + limit: 200 + }; + const result = await this.request.get('clusters/key_pairs', assign(defaultParams, params)); + this.keyPairs = get(result, 'key_pair_set', []); + if (!this.currentPairId || this.currentPairId === this.pairId) { + const nodeIds = get(this.keyPairs[0], 'node_id', ''); + this.currentPairId = get(this.keyPairs[0], 'key_pair_id', ''); + await this.fetchNodes({ node_id: nodeIds }); + } + }; + + @action + addKeyPairs = async (params = {}) => { + if (!this.pairName) { + this.showMsg('Please input Name!'); + } else { + const data = { + name: this.pairName, + mode: this.pairMode, + pub_key: this.pubKey + }; + const result = await this.request.post('clusters/key_pairs', data); + this.apiMsg(result, 'Create SSH Key successful!', async () => { + this.hideModal(); + await this.fetchKeyPairs(); + }); + } + }; + + @action + removeKeyPairs = async () => { + const result = await this.request.delete('clusters/key_pairs', { key_pair_id: [this.pairId] }); + this.hideModal(); + + if (_.get(result, 'key_pair_id')) { + await this.fetchKeyPairs(); + this.showMsg('Delete SSH Key successfully.', 'success'); + } else { + let { err, errDetail } = result; + this.showMsg(errDetail || err); + } + }; } diff --git a/test/components/Base/Tooltip.test.js b/test/components/Base/Tooltip.test.js index 121a99a9..b4c2b13f 100644 --- a/test/components/Base/Tooltip.test.js +++ b/test/components/Base/Tooltip.test.js @@ -24,7 +24,7 @@ describe('Base/Tooltip', () => { ); const instance = wrapper.instance(); - instance.handleTogglePopper(); + instance.handleTogglePopper(instance); expect(mockChange).toHaveBeenCalled(); expect(wrapper.state().visible).not.toBeTruthy(); });