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

This commit is contained in:
wenbo.dong 2017-09-05 17:06:52 +08:00
commit 49e2e5c5b3
26 changed files with 285 additions and 143 deletions

View File

@ -40,4 +40,5 @@ module.exports = {
"strict": 0,
"comma-dangle": ["error", "never"]
}
};
};

View File

@ -31,6 +31,7 @@
"/fekit_modules/*",
"/node_modules/*",
"/bower_components/*",
"/plugins/*",
"/dev/*",
"/prd/*"
]

View File

@ -65,6 +65,7 @@ export default class App extends Component {
<Footer />
</div>
</Router>
)
}
return r;

View File

@ -3,6 +3,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Row, Col } from 'antd';
import { Icon } from 'antd'
import packageJson from '../../../package.json';
class Footer extends Component {
constructor(props) {
super(props)
@ -100,7 +101,7 @@ Footer.defaultProps = {
iconType: 'layout',
linkList: [
{
itemTitle: '版本: 1.0',
itemTitle: '版本: '+packageJson.version,
itemLink: 'javascript:'
}
]

View File

@ -1,64 +0,0 @@
// file_id:2D7ABF69-3BC0-4175-98C9-5C3D5CB00158 -- nerver change this !!
/*
* file: qsso-auth.js
* URL: https://qsso.corp.qunar.com/lib/qsso-auth.js
* written by zhibin.ning
* version: 0.1
*
*/
var AUTH_SERVER = 'https://qsso.corp.qunar.com',
LOGIN_PAGE = '/login.php',
SORRY_PAGE = '/sorry.html';
if (location.hostname.match(/qunar\.ctripgroup\.com$/i)) {
AUTH_SERVER = 'https://qunar.ctripgroup.com/sec/qsso/api';
}
var qualifyURL = function (url, encode) {
url = url || '';
var ret = location.protocol + '//' + location.host + (url.substr(0, 1) === '/' ? '' : location.pathname.match(/.*\//)) + url;
if (encode) {
ret = encodeURIComponent(ret);
}
return ret;
};
var URLStringify = function (o) {
var ret = [];
for (var i in o) {
// ret.push( encodeURIComponent(i) + '=' + encodeURIComponent(o[i]) );
ret.push(i + '=' + o[i]);
}
return ret.join('&');
};
var qsso;
module.exports = qsso = {
'auth': function (loginURI, opt_ext) {
if (!location.hostname.match(/\.qunar(man|ops)?\.com$|\.qunarman\.com$|qunar\.it$|\.928383\.com$|^928383\.com$|qunar\.ctripgroup\.c(om|n)$|\.ctrip(corp)?\.com$|^opsdata\.me$|\.mofun\.com$/i)) {
location.href = AUTH_SERVER + SORRY_PAGE + '?host=' + qualifyURL('', true);
return;
}
var ret = qualifyURL(loginURI, true);
var redirectURL = AUTH_SERVER + LOGIN_PAGE + '?ret=' + ret + (opt_ext ? '&ext=' + encodeURIComponent(URLStringify(opt_ext)) : '');
// console.log(redirectURL);
location.href = redirectURL;
},
'attach': function (eid, loginURI, opt_ext) {
var login = function () {
qsso.auth(loginURI, opt_ext);
};
document.getElementById(eid).onclick = login;
if (location.hash.match('qsso-auto-login')) {
login();
}
}
};

View File

@ -19,7 +19,7 @@ class Subnav extends Component {
<div className="m-subnav">
<Menu
onClick={this.handleClick}
defaultSelectedKeys={[this.props.default]}
selectedKeys={[this.props.default]}
mode="horizontal"
className="g-row m-subnav-menu"
>
@ -29,7 +29,7 @@ class Subnav extends Component {
item.name = item.name[0] + ' ' + item.name[1];
}
return (
<Menu.Item className="item" key={item.name}>
<Menu.Item className="item" key={item.name.replace(' ', '')}>
<Link to={item.path}>{this.props.data[index].name}</Link>
</Menu.Item>
)

View File

@ -7,8 +7,7 @@ import PropTypes from "prop-types";
import { withRouter } from 'react-router';
import { logoSVG, getImgPath } from '../../common.js';
import { changeMenuItem } from '../../reducer/modules/menu'
import Qsso from '../../components/Qsso/Qsso.js'
import Plugins from '../../plugin.js'
const HomeGuest = () => (
<div className="g-body">
<div className="m-bg">
@ -37,7 +36,11 @@ const HomeGuest = () => (
<div className="detail">高效易用功能强大的API管理平台<br /><span className="desc">旨在为开发产品测试人员提供更优雅的接口管理服务</span></div>
<div className="btn-group">
<Link to="/login"><Button type="primary" className="btn-home btn-login">登录 / 注册</Button></Link>
<Button className="btn-home btn-home-normal" id="qsso-login">QSSO 登录</Button>
{Plugins.third_login.listener != null ?
<Plugins.third_login.listener />
: null
}
</div>
</div>
</Col>
@ -181,7 +184,7 @@ class Home extends Component {
}
componentDidMount() {
Qsso.attach('qsso-login', '/api/user/login_by_token')
}
static propTypes = {
introList: PropTypes.array,

View File

@ -68,6 +68,8 @@ $color-bg-lightblue: #c6e2ff;
&:hover, &:focus {
background-color: #f6f9fc;
}
background-color: #fff;
cursor: pointer;
}
}

View File

@ -6,7 +6,7 @@ import { loginActions } from '../../reducer/modules/user';
import { withRouter } from 'react-router'
const FormItem = Form.Item;
import './Login.scss'
import Qsso from '../../components/Qsso/Qsso.js'
// import Qsso from '../../components/Qsso/Qsso.js'
const formItemStyle = {
marginBottom: '.16rem'
@ -54,7 +54,7 @@ class Login extends Component {
}
componentDidMount() {
Qsso.attach('qsso-login','/api/user/login_by_token')
//Qsso.attach('qsso-login','/api/user/login_by_token')
}

View File

@ -79,6 +79,7 @@ class Interface extends Component {
render() {
const { action } = this.props.match.params;
const activeKey = action === 'api' ? 'api' : 'colOrCase';
// console.log(matchPath(this.props.location.pathname, contentRouter));
return <div className="web-content g-row" style={{ marginBottom: "15px" }}>
<Row gutter={16} >
<Col span={6}>

View File

@ -5,6 +5,7 @@
.menu-title {
display: flex;
justify-content: space-between;
overflow: hidden;
.case-delete-icon{
display: none;
}

View File

@ -14,7 +14,9 @@ const TreeNode = Tree.TreeNode;
@connect(
state => {
return {
list: state.inter.list,
inter: state.inter.curdata,
@ -213,8 +215,38 @@ class InterfaceMenu extends Component {
render() {
const matchParams = this.props.match.params;
let menuList = this.props.list;
const searchBox = <div className="interface-filter">
<Input onChange={this.onFilter} value={this.state.filter} placeholder="Filter by name" style={{ width: "70%" }} />
<Tag color="#108ee9" onClick={() => this.changeModal('add_cat_modal_visible', true)} style={{ marginLeft: "15px" }} ><Icon type="plus" /></Tag>
<Modal
title="添加接口"
visible={this.state.visible}
onCancel={() => this.changeModal('visible', false)}
footer={null}
>
<AddInterfaceForm catdata={this.props.curProject.cat} catid={this.state.curCatid} onCancel={() => this.changeModal('visible', false)} onSubmit={this.handleAddInterface} />
</Modal>
<Modal
title="添加分类"
visible={this.state.add_cat_modal_visible}
onCancel={() => this.changeModal('add_cat_modal_visible', false)}
footer={null}
>
<AddInterfaceCatForm onCancel={() => this.changeModal('add_cat_modal_visible', false)} onSubmit={this.handleAddInterfaceCat} />
</Modal>
<Modal
title="修改分类"
visible={this.state.change_cat_modal_visible}
onCancel={() => this.changeModal('change_cat_modal_visible', false)}
footer={null}
>
<AddInterfaceCatForm catdata={this.state.curCatdata} onCancel={() => this.changeModal('change_cat_modal_visible', false)} onSubmit={this.handleChangeInterfaceCat} />
</Modal>
</div>
if(menuList.length === 0){
return null;
return searchBox;
}
const defaultExpandedKeys = () => {
const { router, inter, list } = this.props, rNull = { expands: [], selects: [] };
@ -307,36 +339,7 @@ class InterfaceMenu extends Component {
}
}
return <div>
<div className="interface-filter">
<Input onChange={this.onFilter} value={this.state.filter} placeholder="Filter by name" style={{ width: "70%" }} />
<Tag color="#108ee9" onClick={() => this.changeModal('add_cat_modal_visible', true)} style={{ marginLeft: "15px" }} ><Icon type="plus" /></Tag>
<Modal
title="添加接口"
visible={this.state.visible}
onCancel={() => this.changeModal('visible', false)}
footer={null}
>
<AddInterfaceForm catdata={this.props.curProject.cat} catid={this.state.curCatid} onCancel={() => this.changeModal('visible', false)} onSubmit={this.handleAddInterface} />
</Modal>
<Modal
title="添加分类"
visible={this.state.add_cat_modal_visible}
onCancel={() => this.changeModal('add_cat_modal_visible', false)}
footer={null}
>
<AddInterfaceCatForm onCancel={() => this.changeModal('add_cat_modal_visible', false)} onSubmit={this.handleAddInterfaceCat} />
</Modal>
<Modal
title="修改分类"
visible={this.state.change_cat_modal_visible}
onCancel={() => this.changeModal('change_cat_modal_visible', false)}
footer={null}
>
<AddInterfaceCatForm catdata={this.state.curCatdata} onCancel={() => this.changeModal('change_cat_modal_visible', false)} onSubmit={this.handleChangeInterfaceCat} />
</Modal>
</div>
{searchBox}
{menuList.length > 0 ?
<Tree
className="interface-list"
@ -356,7 +359,7 @@ class InterfaceMenu extends Component {
</Dropdown>
</div>}
key={'cat_' + item._id}
className="interface-item-nav"
className={`interface-item-nav ${item.list.length?"":"cat_switch_hidden"}`}
>
{item.list.map(item_interface_create)}

View File

@ -31,9 +31,10 @@ class View extends Component {
req_body_form(req_body_type,req_body_form){
if(req_body_type === 'json'){
let h = this.countEnter(this.props.curData.req_body_other);
return <div style={{display:this.props.curData.req_body_other?"block":"none"}} className="colBody">
<span className="colKey">请求Body</span>
<div id="vreq_body_json" style={{ minHeight: "200px" }}></div>
<div id="vreq_body_json" style={{ minHeight: h*16+20 }}></div>
</div>
}else if(req_body_type === 'form'){
@ -92,9 +93,10 @@ class View extends Component {
}
res_body(res_body_type,res_body){
if(res_body_type === 'json'){
let h = this.countEnter(this.props.curData.req_body_other);
return <div style={{display:this.props.curData.res_body?"":"none"}} className="colBody">
<span className="colKey">返回Body</span>
<div id="vres_body_json" style={{ minHeight: "200px" }}></div>
<div id="vres_body_json" style={{ minHeight: h*16+20 }}></div>
</div>
}else if(res_body_type === 'raw'){
return <div style={{display:this.props.curData.res_body?"":"none"}} className="colBody">
@ -135,9 +137,19 @@ class View extends Component {
return <Table bordered size="small" pagination = {false} columns= {columns} dataSource = {dataSource} />;
}
countEnter(str){
let i = 0;
let c = 0;
if(!str||!str.indexOf) return 0;
while(str.indexOf('\n',i)>-1){
i = str.indexOf('\n',i) + 2;
c++;
}
return c;
}
bindAceEditor(){
if(this.props.curData.req_body_type === "json"&&this.props.curData.title){
// console.log(this.props.curData.req_body_other.indexOf("\n"));
mockEditor({
container: 'vreq_body_json',
data: this.props.curData.req_body_other,

View File

@ -93,4 +93,9 @@
text-indent: 2em;
}
}
// .ace_scroller{
// position: relative;
// margin-left: 50px;
// overflow:visible;
// }
}

View File

@ -51,6 +51,13 @@
background-color: #efefef
}
.interface-list{
.cat_switch_hidden{
.ant-tree-switcher{
visibility: hidden;
}
}
a{
color: #333
}
@ -85,7 +92,7 @@
}
}
}
.right-content{
margin:3px;
min-height: 5rem;

View File

@ -1,3 +1,4 @@
import './plugin'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './Application'

45
client/plugin.js Normal file
View File

@ -0,0 +1,45 @@
const config = process.env.config;
/**
* type single 只绑定一个监听函数,会返回处理结果
* mulit 绑定多个监听函数
*
*/
var hooks = {
'third_login': {
type: 'single',
listener: null
},
'add_interface': {
type: 'mulit',
listener: []
}
};
function bindHook(name, listener) {
if (!name) throw new Error('缺少hookname');
if (name in hooks === false) {
throw new Error('不存在的hookname');
}
if (hooks[name].type === 'multi') {
hooks[name].listener.push(listener);
} else {
hooks[name].listener = listener;
}
}
var yapi = {
hooks: hooks,
bindHook: bindHook
}
if (config.plugins && Array.isArray(config.plugins)) {
config.plugins.forEach(plugin => {
let pluginModule = require(`plugins/yapi-plugin-${plugin}/client.js`);
pluginModule.call(yapi) ;
})
}
module.exports = hooks;

View File

@ -1,3 +1,3 @@
{
"watch": ["server/"]
"watch": ["server/", "common/", "plugins"]
}

5
npm-shrinkwrap.json generated
View File

@ -15012,6 +15012,11 @@
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yapi-plugin-qsso": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/yapi-plugin-qsso/-/yapi-plugin-qsso-1.0.2.tgz",
"integrity": "sha512-x1LS8R5Vdjep+a93M97v7LcdV+3tupqIOkxxS+uYh+rbYB568BZFEpvfR43L7PcBZcCaOcsCOJEsrw1rcq5rhQ=="
},
"yargs": {
"version": "7.1.0",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/yargs/-/yargs-7.1.0.tgz",

View File

@ -98,6 +98,7 @@
"universal-cookie": "^2.0.8",
"url": "^0.11.0",
"validate-commit-msg": "^2.12.2",
"yapi-plugin-qsso": "^1.0.2",
"ykit-config-antd": "^0.1.3"
},
"devDependencies": {

View File

@ -9,12 +9,14 @@ const bodyParser = require('koa-bodyparser');
const router = require('./router.js');
const websockify = require('koa-websocket');
const websocket = require('./websocket.js');
const plugins = require('./plugin.js');
yapi.connect = dbModule.connect();
const app = websockify(new Koa());
let indexFile = process.argv[2] === 'dev' ? 'dev.html' : 'index.html';
yapi.connect = dbModule.connect();
const app = websockify(new Koa());
yapi.app = app;
plugins();
app.use(mockServer);
app.use(bodyParser({multipart: true}));
app.use(router.routes());

View File

@ -109,11 +109,9 @@ class userController extends baseController {
}
async loginByToken(ctx) {
let config = this.thirdQunarLogin();
let token = ctx.request.body[config.tokenField] || ctx.request.query[config.tokenField];
//let config = this.thirdQunarLogin();
try {
let ret = await config.request(token);
let ret = await yapi.emitHook('third_login', ctx);
let login = await this.handleThirdLogin(ret.email, ret.username);
if (login === true) {

59
server/plugin.js Normal file
View File

@ -0,0 +1,59 @@
const yapi = require('./yapi.js');
const plugin_path = yapi.path.join(yapi.WEBROOT, 'node_modules')
var hooks = {
'third_login': {
type: 'single',
listener: null
},
'add_interface': {
type: 'mulit',
listener: []
}
};
function bindHook(name, listener){
if(!name) throw new Error('缺少hookname');
if(name in hooks === false){
throw new Error('不存在的hookname');
}
if(hooks[name].type === 'multi'){
hooks[name].listener.push(listener);
}else{
hooks[name].listener = listener;
}
}
function emitHook(name){
if(!name) throw new Error('缺少hookname');
if(name in hooks === false){
throw new Error('不存在的hookname');
}
if(hooks[name] && typeof hooks[name] === 'object'){
if(hooks[name].type === 'single' && typeof hooks[name].listener === 'function'){
return hooks[name].listener.apply(yapi, Array.prototype.slice.call(arguments, 1));
}
if(Array.isArray(hooks[name.listener])){
hooks[name].listener.forEach(listener=>{
listener.apply(yapi, Array.prototype.slice.call(arguments,1))
})
}
}
}
yapi.bindHook = bindHook;
yapi.emitHook = emitHook;
module.exports = function(){
if(yapi.WEBCONFIG.plugins && Array.isArray(yapi.WEBCONFIG.plugins)){
yapi.WEBCONFIG.plugins.forEach(plugin=>{
if(!yapi.commons.fileExist(yapi.path.join(plugin_path, 'yapi-plugin-' + plugin + '/server.js'))){
throw new Error(`请安装插件(${plugin}), npm install yapi-plugin-${plugin}`)
}
let pluginModule = require(yapi.path.join(plugin_path, 'yapi-plugin-' + plugin + '/server.js'));
pluginModule.apply(yapi)
})
}
}

View File

@ -39,6 +39,7 @@ function delInst(m) {
}
}
let r = {
fs: fs,
path: path,

View File

@ -5,15 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>YAPI-高效、易用、功能强大的api管理平台</title>
<link rel="icon" type="image/png" sizes="192x192" href="/image/favicon.png">
<link rel="stylesheet" href="http://127.0.0.1:4000/yapi/prd/index@dev.css">
<link rel="stylesheet" href="/prd/index.css">
</head>
<body>
<div id="yapi" style="height: 100%;"></div>
<script src="http://127.0.0.1:4000/prd/manifest@dev.js"></script>
<script src="http://127.0.0.1:4000/prd/lib2@dev.js"></script>
<script src="http://127.0.0.1:4000/prd/lib@dev.js"></script>
<script src="http://127.0.0.1:4000/prd/index@dev.js"></script>
<script src="/prd/manifest.js"></script>
<script src="/prd/lib2.js"></script>
<script src="/prd/lib.js"></script>
<script src="/prd/index.js"></script>

96
ykit.js
View File

@ -1,12 +1,32 @@
var path = require('path');
var fs = require('fs');
var AssetsPlugin = require('assets-webpack-plugin')
var CompressionPlugin = require('compression-webpack-plugin')
var config = require('../config.json');
var assetsPluginInstance = new AssetsPlugin({
filename: 'static/prd/assets.js',
processOutput: function (assets) {
return 'window.WEBPACK_ASSETS = ' + JSON.stringify(assets);
}
})
});
function fileExist (filePath){
try {
return fs.statSync(filePath).isFile();
} catch (err) {
return false;
}
};
function initPlugins(){
if(config.plugins && Array.isArray(config.plugins)){
config.plugins = config.plugins.filter(item=>{
return fileExist(path.resolve(__dirname, 'node_modules/yapi-plugin-' + item + '/client.js'))
})
}
}
initPlugins();
var compressPlugin = new CompressionPlugin({
asset: "[path].gz[query]",
@ -81,21 +101,22 @@ function handleCommonsChunk(webpackConfig) {
module.exports = {
plugins: [{
name: 'antd',
options: {
modifyQuery: function (defaultQuery) { // 可查看和编辑 defaultQuery
defaultQuery.plugins = [];
defaultQuery.plugins.push(["transform-runtime", {
"polyfill": false,
"regenerator": true
}]);
defaultQuery.plugins.push('transform-decorators-legacy');
defaultQuery.plugins.push(["import", { libraryName: "antd"}])
return defaultQuery;
}
}
}],
// plugins: [{
// name: 'antd',
// options: {
// modifyQuery: function (defaultQuery) { // 可查看和编辑 defaultQuery
// defaultQuery.plugins = [];
// defaultQuery.plugins.push(["transform-runtime", {
// "polyfill": false,
// "regenerator": true
// }]);
// defaultQuery.plugins.push('transform-decorators-legacy');
// defaultQuery.plugins.push(["import", { libraryName: "antd"}])
// return defaultQuery;
// },
// exclude: /node_modules(?!\/yapi\-plugin\-)/
// }
// }],
// devtool: 'cheap-source-map',
config: function (ykit) {
return {
@ -119,19 +140,25 @@ module.exports = {
}
baseConfig.plugins.push(new this.webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(ENV_PARAMS)
'process.env.NODE_ENV': JSON.stringify(ENV_PARAMS),
'process.env.config': JSON.stringify(config)
}))
//初始化配置
baseConfig.devtool = 'cheap-module-eval-source-map'
baseConfig.context = path.resolve(__dirname, './client');
baseConfig.resolve.alias.common = '/common';
baseConfig.resolve.alias.plugins = '/node_modules';
baseConfig.output.local.path = 'static/prd';
baseConfig.output.dev.path = 'static/prd';
baseConfig.output.prd.path = 'static/prd';
baseConfig.output.prd.publicPath = '';
baseConfig.output.prd.filename = '[name]@[chunkhash][ext]'
//commonsChunk
handleCommonsChunk.call(this, baseConfig)
handleCommonsChunk.call(this, baseConfig);
baseConfig.module.loaders.push({
test: /\.less$/,
loader: ykit.ExtractTextPlugin.extract(
@ -151,10 +178,39 @@ module.exports = {
})
baseConfig.module.preLoaders.push({
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: "eslint-loader"
exclude: /node_modules|plugins/,
loader: require.resolve('eslint-loader')
});
var testReg = /\.(js|jsx)$/,
exclude = /node_modules(?!\/yapi\-plugin\-)/,
query = {
cacheDirectory: true,
presets: [
["es2015", {"loose": true}],
'es2017',
'stage-0',
'stage-1',
'stage-2',
'react'
],
plugins: []
};
query.plugins.push(["transform-runtime", {
"polyfill": false,
"regenerator": true
}]);
query.plugins.push('transform-decorators-legacy');
query.plugins.push(["import", { libraryName: "antd"}])
baseConfig.module.loaders.push({
loader: require.resolve('babel-loader'),
test: testReg,
exclude: exclude,
query: query
})
if (this.env == 'prd') {
baseConfig.plugins.push(assetsPluginInstance)
baseConfig.plugins.push(compressPlugin)