Merge branch 'dev' of gitlab.corp.qunar.com:mfe/yapi into dev

This commit is contained in:
suxiaoxin 2017-09-26 17:16:57 +08:00
commit 650a25e98e
14 changed files with 289 additions and 47 deletions

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Mock from 'mockjs'
import { Button, Input, Select, Alert, Spin, Icon, Collapse, Tooltip, message, AutoComplete } from 'antd'
import { Button, Input, Select, Alert, Spin, Icon, Collapse, Tooltip, message, AutoComplete, Switch } from 'antd'
import { autobind } from 'core-decorators';
import constants from '../../constants/variable.js'
@ -40,7 +40,7 @@ const mockDataSource = wordList.map(item => {
});
const { TextArea } = Input;
// const { TextArea } = Input;
const InputGroup = Input.Group;
const Option = Select.Option;
const Panel = Collapse.Panel;
@ -71,7 +71,10 @@ export default class Run extends Component {
loading: false,
validRes: [],
hasPlugin: true,
test_status: null
test_status: null,
resTest: false,
resStatusCode: null,
resStatusText: ''
}
constructor(props) {
@ -214,7 +217,12 @@ export default class Run extends Component {
data: bodyType === 'form' ? this.arrToObj(bodyForm) : bodyOther,
files: bodyType === 'form' ? this.getFiles(bodyForm) : {},
file: bodyType === 'file' ? 'single-file' : null,
success: (res, header) => {
success: (res, header, third) => {
console.log('suc',third);
this.setState({
resStatusCode: third.res.status,
resStatusText: third.res.statusText
})
try {
if (isJsonData(header)) {
res = json_parse(res);
@ -259,7 +267,12 @@ export default class Run extends Component {
console.error(e.message)
}
},
error: (err, header) => {
error: (err, header, third) => {
console.log('err',third);
this.setState({
resStatusCode: third.res.status,
resStatusText: third.res.statusText
})
try {
err = json_parse(err);
} catch (e) {
@ -496,6 +509,13 @@ export default class Run extends Component {
console.log(index)
}
@autobind
onTestSwitched(checked) {
this.setState({
resTest: checked
});
}
render() {
const { method, domains, pathParam, pathname, query, headers, bodyForm, caseEnv, bodyType, resHeader, loading, validRes } = this.state;
HTTP_METHOD[method] = HTTP_METHOD[method] || {}
@ -722,30 +742,59 @@ export default class Run extends Component {
</Collapse>
<h2 className="interface-title">返回结果</h2>
<Spin spinning={this.state.loading}>
<div className="res-code"></div>
<Collapse defaultActiveKey={['0', '1']} bordered={true}>
{this.state.resStatusCode ?
<Spin spinning={this.state.loading}>
<h2 className={'res-code ' + ((this.state.resStatusCode >= 200 && this.state.resStatusCode < 400 && !this.state.loading) ? 'success' : 'fail')}>{this.state.resStatusCode + ' ' + this.state.resStatusText}</h2>
<div className="container-header-body">
<div className="header">
<div className="container-title">
<h4>Headers</h4>
</div>
<div id="res-headers-pretty" className="pretty-editor-header"></div>
</div>
<div className="resizer">
<div className="container-title">
<h4 style={{visibility: 'hidden'}}>1</h4>
</div>
</div>
<div className="body">
<div className="container-title">
<h4>Body</h4>
</div>
<div id="res-body-pretty" className="pretty-editor-body" style={{ display: isResJson ? '' : 'none' }}></div>
<div
style={{display: isResJson ? 'none' : ''}}
className="res-body-text"
>{this.state.res && this.state.res.toString()}</div>
</div>
</div>
</Spin> : <p></p>}
{/*<Collapse defaultActiveKey={['0', '1']} bordered={true}>
<Panel header="BODY" key="0" >
<div id="res-body-pretty" className="pretty-editor-body" style={{ display: isResJson ? '' : 'none' }}></div>
<TextArea
<div id="res-body-pretty" className="pretty-editor-body" style={{ display: isResJson ? '' : 'none' }}></div>*/}
{/*<TextArea
style={{ display: isResJson ? 'none' : '' }}
value={this.state.res && this.state.res.toString()}
autosize={{ minRows: 10, maxRows: 20 }}
></TextArea>
<h3 style={{ marginTop: '15px', display: isResJson ? '' : 'none' }}>返回 Body 验证结果</h3>
<div style={{ display: isResJson ? '' : 'none' }}>
{validResView}
</div>
</Panel>
<Panel header="HEADERS" key="1" >
{/*<TextArea
></TextArea>*/}
{/*</Panel>
<Panel header="HEADERS" key="1" >*/}
{/*<TextArea
value={typeof this.state.resHeader === 'object' ? JSON.stringify(this.state.resHeader, null, 2) : this.state.resHeader.toString()}
autosize={{ minRows: 2, maxRows: 10 }}
></TextArea>*/}
<div id="res-headers-pretty" className="pretty-editor-header"></div>
{/*<div id="res-headers-pretty" className="pretty-editor-header"></div>
</Panel>
</Collapse>
</Spin>
</Collapse>*/}
<h2 className="interface-title">数据结构验证
<Switch style={{verticalAlign: 'text-bottom', marginLeft: '8px'}} checked={this.state.resTest} onChange={this.onTestSwitched} />
</h2>
<div className={(isResJson && this.state.resTest) ? '' : 'none' }>
{(isResJson && this.state.resTest) ? validResView : <div><p>若开启此功能则发送请求后在这里查看验证结果</p><p>YApi Response body </p></div>}
</div>
</div>
)
}

View File

@ -2,10 +2,10 @@ import React, { Component } from 'react'
import { Timeline, Spin, Avatar } from 'antd'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { formatTime } from '../../../../common.js';
import { formatTime } from '../../common.js';
import { Link } from 'react-router-dom'
import { fetchNewsData, fetchMoreNews } from '../../../../reducer/modules/news.js'
import ErrMsg from '../../../../components/ErrMsg/ErrMsg.js';
import { fetchNewsData, fetchMoreNews } from '../../reducer/modules/news.js'
import ErrMsg from '../ErrMsg/ErrMsg.js';
function timeago(timestamp) {
let minutes, hours, days, seconds, mouth, year;
@ -78,7 +78,8 @@ class TimeTree extends Component {
loading: PropTypes.bool,
curpage: PropTypes.number,
typeid: PropTypes.number,
curUid: PropTypes.number
curUid: PropTypes.number,
type: PropTypes.string
}
constructor(props) {
@ -95,7 +96,7 @@ class TimeTree extends Component {
if (this.props.curpage <= this.props.newsData.total) {
this.setState({ loading: true });
this.props.fetchMoreNews(this.props.typeid, 'project', this.props.curpage+1, 8).then(function () {
this.props.fetchMoreNews(this.props.typeid, this.props.type, this.props.curpage+1, 8).then(function () {
that.setState({ loading: false });
if (that.props.newsData.total === that.props.curpage) {
that.setState({ bidden: "logbidden" })
@ -106,7 +107,7 @@ class TimeTree extends Component {
componentWillMount() {
this.props.fetchNewsData(this.props.typeid, 'project', 1, 8)
this.props.fetchNewsData(this.props.typeid, this.props.type, 1, 8)
}
render() {

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import GroupList from './GroupList/GroupList.js';
import ProjectList from './ProjectList/ProjectList.js';
import MemberList from './MemberList/MemberList.js';
import GroupLog from './GroupLog/GroupLog.js';
import { Route, Switch, Redirect } from 'react-router-dom';
import { Tabs, Layout } from 'antd';
const { Content, Sider } = Layout;
@ -27,6 +28,7 @@ export default class Group extends Component {
<Tabs type="card" className="m-tab" style={{height: '100%'}}>
<TabPane tab="项目列表" key="1"><ProjectList/></TabPane>
<TabPane tab="成员列表" key="2"><MemberList/></TabPane>
<TabPane tab="分组动态" key="3"><GroupLog/></TabPane>
</Tabs>
</Content>
</Layout>

View File

@ -0,0 +1,36 @@
import React, { Component } from 'react'
import TimeTree from '../../../components/TimeLine/TimeLine'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
// import { Button } from 'antd'
@connect(
state => {
console.log(state);
return {
uid: state.user.uid + '',
curGroupId: state.group.currGroup._id
}
}
)
class GroupLog extends Component {
constructor(props) {
super(props);
}
static propTypes = {
uid: PropTypes.string,
match: PropTypes.object,
curGroupId: PropTypes.number
}
render () {
return (
<div className="g-row">
<section className="news-box m-panel">
<TimeTree type={"group"} typeid = {this.props.curGroupId} />
</section>
</div>
)
}
}
export default GroupLog;

View File

@ -1,6 +1,6 @@
import './Activity.scss'
import React, { Component } from 'react'
import TimeTree from './Timeline/Timeline'
import TimeTree from '../../../components/TimeLine/TimeLine'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { Button } from 'antd'
@ -41,7 +41,7 @@ class Activity extends Component {
<Button type="primary"><a href = {`/api/project/download?project_id=${this.props.match.params.id}`}>下载Mock数据</a></Button>
</div>
</div>
<TimeTree typeid = {+this.props.match.params.id} />
<TimeTree type={"project"} typeid = {+this.props.match.params.id} />
</section>
</div>
)

View File

@ -127,9 +127,9 @@ class InterfaceColContent extends Component {
status = 'ok';
} else if (result.code === 1) {
status = 'invalid'
}
}
} catch (e) {
status = 'error';
status = 'error';
result = e;
}
this.reports[curitem._id] = result;
@ -177,7 +177,7 @@ class InterfaceColContent extends Component {
headers: that.getHeadersObj(interfaceData.req_headers),
data: interfaceData.req_body_type === 'form' ? that.arrToObj(interfaceData.req_body_form) : interfaceData.req_body_other,
success: (res, header) => {
res = json_parse(res);
res = json_parse(res);
result.res_header = header;
result.res_body = res;
if (res && typeof res === 'object') {
@ -206,7 +206,7 @@ class InterfaceColContent extends Component {
} catch (e) {
console.log(e)
}
err = err || '请求异常';
result.code = 400;
result.res_header = header;
@ -442,7 +442,7 @@ class InterfaceColContent extends Component {
return (
<div className="interface-col">
<h2 style={{ marginBottom: '10px', display: 'inline-block' }}>测试集合&nbsp;<a target="_blank" rel="noopener noreferrer" href="https://yapi.ymfe.org/case.html" >
<h2 className="interface-title" style={{ display: 'inline-block', margin: 0, marginBottom: '16px' }}>测试集合&nbsp;<a target="_blank" rel="noopener noreferrer" href="https://yapi.ymfe.org/case.html" >
<Tooltip title="点击查看文档"><Icon type="question-circle-o" /></Tooltip>
</a></h2>
<Button type="primary" style={{ float: 'right' }} onClick={this.executeTests}>开始测试</Button>
@ -478,4 +478,4 @@ class InterfaceColContent extends Component {
}
}
export default InterfaceColContent
export default InterfaceColContent

View File

@ -89,12 +89,11 @@
}
.interface-col{
padding: 16px;
padding: 24px;
.interface-col-table-header{
background-color: rgb(238, 238, 238);
height: 50px;
line-height: 50px;
font-size:14px;
text-align: left;
}

View File

@ -1,3 +1,5 @@
@import '../../../../styles/mixin.scss';
.interface-edit{
padding: 24px;
.interface-edit-item{
@ -61,3 +63,71 @@
white-space: normal;
word-break: break-all;
}
// 容器左侧是header 右侧是body
.container-header-body {
display: flex;
padding-bottom: .36rem;
.header, .body {
flex: 1 0 300px;
.pretty-editor-header, .pretty-editor-body {
height: 100%;
}
.postman .pretty-editor-body {
min-height: 200px;
}
.ace_print-margin {
display: none;
}
}
.header {
max-width: 400px;
}
.container-title {
padding: .08rem 0;
}
.resizer {
flex: 0 0 21px;
position: relative;
&:after {
content: '';
display: block;
width: 1px;
height: 100%;
background-color: $color-text-dark;
opacity: .8;
position: absolute;
left: 10px;
}
}
// res body 无返回json时显示text信息
.res-body-text {
height: 100%;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 8px;
}
}
.ant-spin-blur {
.res-code.success {
background-color: transparent;
}
.res-code.fail {
background-color: transparent;
}
}
.res-code {
padding: .08rem .28rem;
color: #fff;
margin-left: -.28rem;
margin-right: -.28rem;
transition: all .2s;
position: relative;
border-radius: 2px;
}
.res-code.success {
background-color: $color-antd-green;
}
.res-code.fail {
background-color: $color-antd-red;
}

View File

@ -277,7 +277,7 @@ class View extends Component {
return <p style={{whiteSpace: 'pre-wrap'}}>{item.example}</p>;
}
}, {
title: '备注',
title: '备注',
dataIndex: 'desc',
key: 'desc',
render(_, item) {
@ -304,11 +304,11 @@ class View extends Component {
<Row className="row">
<Col span={4} className="colKey">接口名称</Col>
<Col span={8}>{this.props.curData.title}</Col>
<Col span={4} className="colKey">建人</Col>
<Col span={4} className="colKey">&ensp;&ensp;</Col>
<Col span={8} className="colValue"><Link className="user-name" to={"/user/profile/" + this.props.curData.uid} ><img src={'/api/user/avatar?uid=' + this.props.curData.uid} className="user-img" />{this.props.curData.username}</Link></Col>
</Row>
<Row className="row">
<Col span={4} className="colKey"> </Col>
<Col span={4} className="colKey">&emsp;&emsp;</Col>
<Col span={8} className={'tag-status ' + this.props.curData.status}>{status[this.props.curData.status]}</Col>
<Col span={4} className="colKey">更新时间</Col>
<Col span={8}>{formatTime(this.props.curData.up_time)}</Col>

View File

@ -149,6 +149,17 @@
.right-content{
min-height: 5rem;
background: #fff;
.caseContainer {
table {
border-radius: 4px;
// border-collapse: collapse;
}
.ant-table-small .ant-table-thead > tr > th {
text-align: left;
background-color: #f8f8f8;
}
tr:nth-child(even){background: #f8f8f8;}
}
.interface-content{
.ant-tabs-nav{
width:100%;
@ -195,11 +206,17 @@
}
th {
text-align: left;
font-weight: normal;
background-color: #f8f8f8;
text-indent: .4em;
}
tr {
text-indent: .4em;
}
th, td {
border: 1px solid #e9e9e9;
padding: 8px;
}
tr:nth-child(odd){background: rgba(236, 238, 241, 0.67);}
tr:nth-child(odd){background: #f8f8f8;}
tr:nth-child(even){background: #fff;}
}
}

View File

@ -234,7 +234,7 @@ class ProjectMember extends Component {
</Row>
</Modal>
<Table columns={columns} dataSource={this.state.projectMemberList} pagination={false} locale={{emptyText: <ErrMsg type="noMemberInProject"/>}} className="setting-project-member"/>
<Card title={this.state.groupName + ' 分组成员 ' + '(' + this.state.groupMemberList.length + ') 人'} noHovering className="setting-group">
<Card bordered={false} title={this.state.groupName + ' 分组成员 ' + '(' + this.state.groupMemberList.length + ') 人'} noHovering className="setting-group">
{this.state.groupMemberList.length ? this.state.groupMemberList.map((item, index) => {
return (<div key={index} className="card-item">
<img src={location.protocol + '//' + location.host + '/api/user/avatar?uid=' + item.uid} className="item-img" />

View File

@ -25,14 +25,15 @@
}
}
.setting-project-member {
border: 1px solid #e9e9e9;
border-radius: 2px;
}
// .setting-project-member {
// border: 1px solid #e9e9e9;
// border-radius: 4px;
// }
.setting-group {
margin-top: .48rem;
border-radius: 2px;
border-bottom: 1px solid #eee;
.ant-card-head {
background-color: #eee;
padding: 0 .08rem !important;

View File

@ -8,6 +8,16 @@ $color-black-light: #404040;
$color-bg-dark: #32363a; // 背景色 - header 用的深蓝色
$box-shadow-panel: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
$color-text-dark: rgba(13, 27, 62, 0.65);
$color-antd-green: #00a854;
$color-antd-yellow: #ffbf00;
$color-antd-red: #f56a00;
$color-antd-pink: #f5317f;
$color-antd-cyan: #00a2ae;
$color-antd-gray: #bfbfbf;
$color-antd-purple: #7265e6;
@mixin row-width-limit {
max-width: 12.2rem;
min-width: 10.2rem;

View File

@ -100,6 +100,14 @@ class groupController extends baseController {
let result = await groupInst.save(data);
result = yapi.commons.fieldSelect(result, ['_id', 'group_name', 'group_desc', 'uid', 'members']);
let username = this.getUsername();
yapi.commons.saveLog({
content: `用户 "${username}" 新增了分组 "${params.group_name}"`,
type: 'group',
uid: this.getUid(),
username: username,
typeid: result._id
});
ctx.body = yapi.commons.resReturn(result);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
@ -165,6 +173,19 @@ class groupController extends baseController {
delete groupUserdata._role;
try {
let result = await groupInst.addMember(params.id, groupUserdata);
let username = this.getUsername();
let rolename = {
owner: "组长",
dev: "开发者",
guest: "访客"
};
yapi.commons.saveLog({
content: `用户 "${username}" 新增了分组成员 "${groupUserdata.username}" 为 "${rolename[params.role]}"`,
type: 'group',
uid: this.getUid(),
username: username,
typeid: params.id
});
ctx.body = yapi.commons.resReturn(result);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
@ -205,6 +226,20 @@ class groupController extends baseController {
try {
let result = await groupInst.changeMemberRole(params.id, params.member_uid, params.role);
let username = this.getUsername();
let rolename = {
owner: "组长",
dev: "开发者",
guest: "访客"
};
let groupUserdata = await this.getUserdata(params.member_uid, params.role);
yapi.commons.saveLog({
content: `用户 "${username}" 更改了分组成员 "${groupUserdata.username}" 的权限为 "${rolename[params.role]}"`,
type: 'group',
uid: this.getUid(),
username: username,
typeid: params.id
});
ctx.body = yapi.commons.resReturn(result);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
@ -267,6 +302,20 @@ class groupController extends baseController {
try {
let result = await groupInst.delMember(params.id, params.member_uid);
let username = this.getUsername();
let rolename = {
owner: "组长",
dev: "开发者",
guest: "访客"
};
let groupUserdata = await this.getUserdata(params.member_uid, params.role);
yapi.commons.saveLog({
content: `用户 "${username}" 删除了分组成员 "${groupUserdata.username}"`,
type: 'group',
uid: this.getUid(),
username: username,
typeid: params.id
});
ctx.body = yapi.commons.resReturn(result);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
@ -379,6 +428,14 @@ class groupController extends baseController {
ctx.body = yapi.commons.resReturn(null, 404, '分组名和分组描述不能为空');
}
let result = await groupInst.up(id, data);
let username = this.getUsername();
yapi.commons.saveLog({
content: `用户 "${username}" 更新了 "${data.group_name}" 分组`,
type: 'group',
uid: this.getUid(),
username: username,
typeid: id
});
ctx.body = yapi.commons.resReturn(result);
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 402, err.message);