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 {
- {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 (
+
+ );
+ };
+
+ renderForm() {
+ const { t } = this.props;
+
+ return (
+
+ );
+ }
+
+ 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();
});