feat: local mock service (#62)

This commit is contained in:
bqy_fe 2022-06-06 01:51:08 +08:00
parent a9b61b76c9
commit ef815767b3
28 changed files with 3046 additions and 2233 deletions

View File

@ -19,17 +19,21 @@
"test:workbench": "cd src/workbench/browser && yarn test",
"electron:serve": "wait-on tcp:4200 && npm run electron:dev",
"electron:dev:static": "npm run electron:tsc && electron .",
"electron:dev": "npm run electron:tsc && electron . --development",
"electron:dev": "npm run electron:tsc && npm run copyfile:out && electron . --development",
"build": "npm-run-all -s build:workbench electron:tsc && electron-builder build",
"build:static": "npm run electron:tsc && electron-builder build",
"release": "npm-run-all -s build:workbench electron:tsc && electron-builder --publish=always",
"test": "npm-run-all --serial test:*",
"electron:tsc": "tsc -p tsconfig.json"
"electron:tsc": "tsc -p tsconfig.json",
"copyfile:out": "copyfiles -u 1 src/**/*.json src/app/common/images/** out"
},
"dependencies": {
"@bqy/node-module-alias": "^1.0.1",
"@electron/remote": "2.0.8",
"@koa/cors": "3.3.0",
"@koa/router": "10.1.1",
"content-disposition": "^0.5.4",
"copyfiles": "2.4.1",
"cross-spawn": "^7.0.3",
"crypto-js": "^4.1.1",
"dexie": "3.2.2",
@ -38,13 +42,19 @@
"fix-path": "3.0.0",
"form-data": "^4.0.0",
"iconv-lite": "^0.6.3",
"koa": "2.13.4",
"koa-bodyparser": "4.3.0",
"mockjs": "1.1.0",
"npm": "6.14.17",
"portfinder": "1.0.28",
"resolve": "^1.22.0",
"rxjs": "7.5.5",
"xml2js": "^0.4.23"
},
"devDependencies": {
"@types/cross-spawn": "6.0.2",
"@types/koa": "2.13.4",
"@types/koa__router": "8.0.11",
"@types/node": "17.0.32",
"@typescript-eslint/eslint-plugin": "5.23.0",
"@typescript-eslint/parser": "5.23.0",

View File

@ -12,13 +12,16 @@ import { deleteFile, readJson } from 'eo/shared/node/file';
import { STORAGE_TEMP as storageTemp } from 'eo/shared/common/constant';
import { UnitWorkerModule } from 'eo/workbench/node/unitWorker';
import Configuration from 'eo/platform/node/configuration/lib';
import { ConfigurationInterface } from 'eo/platform/node/configuration';
import { ConfigurationInterface } from 'src/platform/node/configuration';
import { MockServer } from 'eo/platform/node/mock-server';
let win: BrowserWindow = null;
export const subView = {
appView: null,
mainView: null,
};
const eoUpdater = new EoUpdater();
const mockServer = new MockServer();
const moduleManager: ModuleManagerInterface = ModuleManager();
const configuration: ConfigurationInterface = Configuration();
// Remote
@ -85,9 +88,11 @@ try {
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
// Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947
app.on('ready', () => {
app.on('ready', async () => {
setTimeout(createWindow, 400);
eoUpdater.check();
// 启动mock服务
await mockServer.start();
});
//!TODO only api manage app need this
// setupUnit(subView.appView);
@ -99,6 +104,7 @@ try {
if (process.platform !== 'darwin') {
app.quit();
}
mockServer.stop();
});
app.on('activate', () => {
@ -204,6 +210,20 @@ try {
returnValue = configuration.getModuleSettings(arg.data.moduleID);
} else if (arg.action === 'getSidePosition') {
returnValue = subView.appView?.sidePosition;
// 注册单个mock路由
} else if (arg.action === 'registerMockRoute') {
const { method, path, data } = arg.data;
returnValue = mockServer.registerRoute(method, path, data);
// 注销mock路由
} else if (arg.action === 'unRegisterMockRoute') {
const { method, path } = arg.data;
returnValue = mockServer.unRegisterRoute(method, path);
// 获取mock服务地址
} else if (arg.action === 'getMockUrl') {
returnValue = mockServer.getMockUrl();
// 重置并初始化mock路由
} else if (arg.action === 'resetAndInitRoutes') {
returnValue = mockServer.resetAndInitRoutes();
} else if (arg.action === 'hook') {
returnValue = 'hook返回';
} else {

View File

@ -8,7 +8,7 @@ interface StorageModel {
* UUIDUUID或数值型
* @type {string|number}
*/
uuid?: string|number;
uuid?: string | number;
/**
*
@ -49,7 +49,7 @@ export interface Environment extends StorageModel {
* ID
* @type {string|number}
*/
projectID: string|number;
projectID: string | number;
/**
* url
@ -64,7 +64,6 @@ export interface Environment extends StorageModel {
parameters?: object;
}
/**
*
*/
@ -79,13 +78,13 @@ export interface Group extends StorageModel {
* ID
* @type {string|number}
*/
projectID: string|number;
projectID: string | number;
/**
* 0
* @type {string|number}
*/
parentID?: string|number;
parentID?: string | number;
/**
*
@ -391,6 +390,10 @@ export interface ApiData extends StorageModel {
* @type {JsonRootType|string}
*/
responseBodyJsonType?: JsonRootType | string;
/**
* mock列表
*/
mockList?: ApiEditMock[];
}
/**
@ -450,6 +453,18 @@ export interface ParamsEnum {
*/
description: string;
}
export type ApiEditMock = {
/** mock名称 */
name: string;
/** mock地址 */
url: string;
/** mock返回值 */
response: string;
/** 是否系统默认mock */
isDefault?: boolean;
};
export interface BasiApiEditParams {
/**
*
@ -512,56 +527,59 @@ export interface ApiEditBody extends BasiApiEditParams {
export interface StorageInterface {
// Project
projectCreate: (item: Project) => Observable<object>;
projectUpdate: (item: Project, uuid: number|string) => Observable<object>;
projectUpdate: (item: Project, uuid: number | string) => Observable<object>;
projectBulkUpdate: (items: Array<Project>) => Observable<object>;
projectRemove: (uuid: number|string) => Observable<boolean>;
projectBulkRemove: (uuids: Array<number|string>) => Observable<boolean>;
projectLoad: (uuid: number|string) => Observable<object>;
projectBulkLoad: (uuids: Array<number|string>) => Observable<Array<object>>;
projectRemove: (uuid: number | string) => Observable<boolean>;
projectBulkRemove: (uuids: Array<number | string>) => Observable<boolean>;
projectLoad: (uuid: number | string) => Observable<object>;
projectBulkLoad: (uuids: Array<number | string>) => Observable<Array<object>>;
projectExport: () => Observable<object>;
// Environment
environmentCreate: (item: Environment) => Observable<object>;
environmentUpdate: (item: Environment, uuid: number|string) => Observable<object>;
environmentUpdate: (item: Environment, uuid: number | string) => Observable<object>;
environmentBulkCreate: (items: Array<Environment>) => Observable<object>;
environmentBulkUpdate: (items: Array<Environment>) => Observable<object>;
environmentRemove: (uuid: number|string) => Observable<boolean>;
environmentBulkRemove: (uuids: Array<number|string>) => Observable<boolean>;
environmentLoad: (uuid: number|string) => Observable<object>;
environmentBulkLoad: (uuids: Array<number|string>) => Observable<Array<object>>;
environmentLoadAllByProjectID: (projectID: number|string) => Observable<Array<object>>;
environmentRemove: (uuid: number | string) => Observable<boolean>;
environmentBulkRemove: (uuids: Array<number | string>) => Observable<boolean>;
environmentLoad: (uuid: number | string) => Observable<object>;
environmentBulkLoad: (uuids: Array<number | string>) => Observable<Array<object>>;
environmentLoadAllByProjectID: (projectID: number | string) => Observable<Array<object>>;
// Group
groupCreate: (item: Group) => Observable<object>;
groupUpdate: (item: Group, uuid: number|string) => Observable<object>;
groupUpdate: (item: Group, uuid: number | string) => Observable<object>;
groupBulkCreate: (items: Array<Group>) => Observable<object>;
groupBulkUpdate: (items: Array<Group>) => Observable<object>;
groupRemove: (uuid: number|string) => Observable<boolean>;
groupBulkRemove: (uuids: Array<number|string>) => Observable<boolean>;
groupLoad: (uuid: number|string) => Observable<object>;
groupBulkLoad: (uuids: Array<number|string>) => Observable<Array<object>>;
groupLoadAllByProjectID: (projectID: number|string) => Observable<Array<object>>;
groupRemove: (uuid: number | string) => Observable<boolean>;
groupBulkRemove: (uuids: Array<number | string>) => Observable<boolean>;
groupLoad: (uuid: number | string) => Observable<object>;
groupBulkLoad: (uuids: Array<number | string>) => Observable<Array<object>>;
groupLoadAllByProjectID: (projectID: number | string) => Observable<Array<object>>;
// Api Data
apiDataCreate: (item: ApiData) => Observable<object>;
apiDataUpdate: (item: ApiData, uuid: number|string) => Observable<object>;
apiDataUpdate: (item: ApiData, uuid: number | string) => Observable<object>;
apiDataBulkCreate: (items: Array<ApiData>) => Observable<object>;
apiDataBulkUpdate: (items: Array<ApiData>) => Observable<object>;
apiDataRemove: (uuid: number|string) => Observable<boolean>;
apiDataBulkRemove: (uuids: Array<number|string>) => Observable<boolean>;
apiDataLoad: (uuid: number|string) => Observable<object>;
apiDataBulkLoad: (uuids: Array<number|string>) => Observable<Array<object>>;
apiDataLoadAllByProjectID: (projectID: number|string) => Observable<Array<object>>;
apiDataLoadAllByGroupID: (groupID: number|string) => Observable<Array<object>>;
apiDataLoadAllByProjectIDAndGroupID: (projectID: number|string, groupID: number|string) => Observable<Array<object>>;
apiDataRemove: (uuid: number | string) => Observable<boolean>;
apiDataBulkRemove: (uuids: Array<number | string>) => Observable<boolean>;
apiDataLoad: (uuid: number | string) => Observable<object>;
apiDataBulkLoad: (uuids: Array<number | string>) => Observable<Array<object>>;
apiDataLoadAllByProjectID: (projectID: number | string) => Observable<Array<object>>;
apiDataLoadAllByGroupID: (groupID: number | string) => Observable<Array<object>>;
apiDataLoadAllByProjectIDAndGroupID: (
projectID: number | string,
groupID: number | string
) => Observable<Array<object>>;
// Api Test History
apiTestHistoryCreate: (item: ApiTestHistory) => Observable<object>;
apiTestHistoryUpdate: (item: ApiTestHistory, uuid: number|string) => Observable<object>;
apiTestHistoryUpdate: (item: ApiTestHistory, uuid: number | string) => Observable<object>;
apiTestHistoryBulkCreate: (items: Array<ApiTestHistory>) => Observable<object>;
apiTestHistoryBulkUpdate: (items: Array<ApiTestHistory>) => Observable<object>;
apiTestHistoryRemove: (uuid: number|string) => Observable<boolean>;
apiTestHistoryBulkRemove: (uuids: Array<number|string>) => Observable<boolean>;
apiTestHistoryLoad: (uuid: number|string) => Observable<object>;
apiTestHistoryBulkLoad: (uuids: Array<number|string>) => Observable<Array<object>>;
apiTestHistoryLoadAllByProjectID: (projectID: number|string) => Observable<Array<object>>;
apiTestHistoryLoadAllByApiDataID: (apiDataID: number|string) => Observable<Array<object>>;
apiTestHistoryRemove: (uuid: number | string) => Observable<boolean>;
apiTestHistoryBulkRemove: (uuids: Array<number | string>) => Observable<boolean>;
apiTestHistoryLoad: (uuid: number | string) => Observable<object>;
apiTestHistoryBulkLoad: (uuids: Array<number | string>) => Observable<Array<object>>;
apiTestHistoryLoadAllByProjectID: (projectID: number | string) => Observable<Array<object>>;
apiTestHistoryLoadAllByApiDataID: (apiDataID: number | string) => Observable<Array<object>>;
}
export type StorageItem = Project | Environment | Group | ApiData | ApiTestHistory;
@ -583,11 +601,11 @@ export enum StorageHandleStatus {
success = 'success',
empty = 'empty',
error = 'error',
invalid = 'invalid'
invalid = 'invalid',
}
export enum StorageProcessType {
default = 'default',
remote = 'remote',
sync = 'sync'
}
sync = 'sync',
}

View File

@ -147,3 +147,22 @@ window.eo.getSettings = (settings) => {
window.eo.getModuleSettings = (moduleID) => {
return ipcRenderer.sendSync('eo-sync', { action: 'getModuleSettings', data: { moduleID: moduleID } });
};
// 注册单个mock路由
window.eo.registerMockRoute = ({ method, path, data }) => {
return ipcRenderer.sendSync('eo-sync', { action: 'registerMockRoute', data: { method, path, data } });
};
// 注销mock路由
window.eo.unRegisterMockRoute = ({ method, path }) => {
return ipcRenderer.sendSync('eo-sync', { action: 'unRegisterMockRoute', data: { method, path } });
};
// 获取mock服务地址
window.eo.getMockUrl = () => {
return ipcRenderer.sendSync('eo-sync', { action: 'getMockUrl' });
};
// 重置并初始化路由
window.eo.resetAndInitRoutes = () => {
return ipcRenderer.sendSync('eo-sync', { action: 'resetAndInitRoutes' });
};

View File

@ -59,7 +59,7 @@ export class Configuration implements ConfigurationInterface {
data.nestedSettings ??= {};
data.settings[moduleID] = settings;
const propArr = moduleID.split('.');
const target = propArr.slice(0, -1).reduce((p, k) => p[k], data.nestedSettings);
const target = propArr.slice(0, -1).reduce((p, k) => p?.[k], data.nestedSettings);
target[propArr.at(-1)] = settings;
return this.saveConfig(data);
}

View File

@ -0,0 +1,53 @@
const Koa = require('koa');
const Router = require('koa-router');
const glob = require('glob');
const logger = require('koa-logger');
const { resolve } = require('path');
const fs = require('fs');
const app = new Koa();
const router = new Router({ prefix: '/api' });
const routerMap = {}; // 存放路由映射
app.use(logger());
// 注册路由
glob.sync(resolve('./api', '**/*.json')).forEach((item, i) => {
let apiJsonPath = item && item.split('/api')[1];
let apiPath = apiJsonPath.replace('.json', '');
router.get(apiPath, (ctx, next) => {
try {
let jsonStr = fs.readFileSync(item).toString();
ctx.body = {
data: JSON.parse(jsonStr),
state: 200,
type: 'success', // 自定义响应体
};
} catch (err) {
ctx.throw('服务器错误', 500);
}
});
// 记录路由
routerMap[apiJsonPath] = apiPath;
});
fs.writeFile('./routerMap.json', JSON.stringify(routerMap, null, 4), (err) => {
if (!err) {
console.log('路由地图生成成功!');
}
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3040);
// const Koa = require('koa');
// const app = new Koa();
// app.use(async (ctx) => {
// ctx.body = 'hello koa2';
// });
// app.listen(3000);
// console.log('[demo] start-quick is starting at port 3000');

View File

@ -0,0 +1,146 @@
import Koa from 'koa';
import Router from '@koa/router';
import cors from '@koa/cors';
import bodyParser from 'koa-bodyparser';
import portfinder from 'portfinder';
import mockjs from 'mockjs';
export class MockServer {
private app: Koa;
private router: Router;
/** mock服务地址 */
private mockUrl = '';
constructor(prefix = '') {
this.app = new Koa();
this.router = new Router({ prefix });
// 使用ctx.body解析中间件
this.app.use(bodyParser());
// 加载路由中间件
this.app.use(this.router.routes()).use(this.router.allowedMethods());
// 允许跨域请求
this.app.use(cors());
this.initRoutes();
}
/**
* mock服务
* @param port mock服务端口号
*/
async start(port = 3040) {
portfinder.basePort = port;
// 使用 portfinder 做端口检测若发现端口被占用则端口自增1
const _port = await portfinder.getPortPromise();
return new Promise((resolve, reject) => {
this.app
.listen(_port, () => {
this.mockUrl = `http://localhost:${_port}`;
console.log(`mock服务已启动: ${this.mockUrl}`);
resolve(this.mockUrl);
})
.on('error', (error) => {
console.error('mock服务启动失败: ' + error);
reject(error);
});
});
}
/**
* mock服务
*/
stop() {
process.exit(1);
}
/**
* mock服务地址
* @returns mock服务地址
*/
getMockUrl() {
return this.mockUrl;
}
/**
* \
*/
resetRoutes() {
this.router.stack = [];
}
/**
* \
*/
resetAndInitRoutes() {
this.resetRoutes();
this.initRoutes();
}
/**
*
* @param method
* @param path
* @param data
*/
registerRoute(method: string, path: string, data = {}) {
const { pathname, search } = new URL(path, this.mockUrl);
// Object.fromEntries(searchParams.entries())
// console.log('registerRoute', method.toLocaleLowerCase(), pathname + search);
this.router[method.toLocaleLowerCase()](pathname + search, async (ctx, next) => {
try {
const mockData = typeof data === 'string' ? JSON.parse(data) : data;
ctx.body = mockjs.mock(mockData);
} catch (e) {
ctx.body = {
tips: '返回数据格式有误,请检查!',
errorMsg: e.message,
originData: data,
};
ctx.status = 500;
}
await next();
});
}
/**
*
* @param methods
* @param path
*/
unRegisterRoute(methods: string[], path: string) {
const _methods = methods.map((n) => n.toLocaleUpperCase());
// 将匹配到的路由注销掉
this.router.stack = this.router.stack.filter((item) => {
const isMatch = item.methods.some((n) => _methods.includes(n)) && item.path === path;
return !isMatch;
});
}
/**
*
*/
initRoutes() {
this.router.get('/', async (ctx, next) => {
const mockPeople = mockjs.mock({
'peoples|10': [
{
'id|+1': 1,
guid: '@guid',
name: '@cname',
age: '@integer(20, 50)',
birthday: '@date("MM-dd")',
address: '@county(true)',
email: '@email',
},
],
});
ctx.body = mockPeople;
await next();
});
this.router.get('/mock_stack', async (ctx, next) => {
ctx.body = this.router;
await next();
});
}
}

View File

@ -20,13 +20,11 @@
<div *ngIf="apiData.requestBody?.length">
<div class="api_line">
Body 请求参数<nz-tag class="ml10" nzColor="default">{{ CONST.BODY_TYPE[apiData.requestBodyType] }}</nz-tag>
<nz-tag *ngIf="apiData.requestBodyType==='json'" nzColor="default">最外层结构为:{{ CONST.JSON_ROOT_TYPE[apiData.requestBodyJsonType] }}</nz-tag>
<nz-tag *ngIf="apiData.requestBodyType==='json'" nzColor="default">最外层结构为:{{
CONST.JSON_ROOT_TYPE[apiData.requestBodyJsonType] }}</nz-tag>
</div>
<eo-api-detail-body
[bodyType]="apiData.requestBodyType"
[model]="apiData.requestBody"
[jsonRootType]="apiData.requestBodyJsonType"
></eo-api-detail-body>
<eo-api-detail-body [bodyType]="apiData.requestBodyType" [model]="apiData.requestBody"
[jsonRootType]="apiData.requestBodyJsonType"></eo-api-detail-body>
</div>
<div *ngIf="apiData.responseHeaders && apiData.responseHeaders.length">
<p class="api_line">返回头部</p>
@ -34,10 +32,11 @@
</div>
<div *ngIf="apiData.responseBody && apiData.responseBody.length">
<p class="api_line">返回参数</p>
<eo-api-detail-body
[bodyType]="apiData.responseBodyType"
[model]="apiData.responseBody"
[jsonRootType]="apiData.responseBodyJsonType"
></eo-api-detail-body>
<eo-api-detail-body [bodyType]="apiData.responseBodyType" [model]="apiData.responseBody"
[jsonRootType]="apiData.responseBodyJsonType"></eo-api-detail-body>
</div>
<div>
<p class="api_line">MOCK </p>
<eo-api-detail-mock [model]="apiData.mockList"></eo-api-detail-mock>
</div>
</section>

View File

@ -14,6 +14,8 @@
margin-bottom: 10px;
font-size: 16px;
font-weight: bold;
border-left: 3px solid var(--GREEN_NORMAL);
text-indent: 5px;
}
.t-1 {

View File

@ -9,38 +9,30 @@ import { SharedModule } from '../../../shared/shared.module';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { NzFormModule } from 'ng-zorro-antd/form';
import { ApiDetailComponent } from './api-detail.component';
import { ApiDetailHeaderComponent } from './header/api-detail-header.component';
import { ApiDetailBodyComponent } from './body/api-detail-body.component';
import { ApiDetailQueryComponent } from './query/api-detail-query.component';
import { ApiDetailRestComponent } from './rest/api-detail-rest.component';
import { ApiDetailMockComponent } from './mock/api-detail-mock.component';
import { ApiDetailService } from './api-detail.service';
const NZ_COMPONETS = [
NzButtonModule,
NzIconModule,
NzTagModule
];
const NZ_COMPONETS = [NzButtonModule, NzIconModule, NzTagModule, NzModalModule, NzFormModule];
const COMPONENTS = [
ApiDetailComponent,
ApiDetailHeaderComponent,
ApiDetailBodyComponent,
ApiDetailQueryComponent,
ApiDetailRestComponent
ApiDetailRestComponent,
ApiDetailMockComponent,
];
@NgModule({
declarations: [...COMPONENTS],
imports: [
FormsModule,
ReactiveFormsModule,
Ng1Module,
CommonModule,
...NZ_COMPONETS,
EouiModule,
SharedModule,
],
providers:[ApiDetailService]
imports: [FormsModule, ReactiveFormsModule, Ng1Module, CommonModule, ...NZ_COMPONETS, EouiModule, SharedModule],
providers: [ApiDetailService],
})
export class ApiDetailModule {}

View File

@ -0,0 +1,5 @@
<eo-table [(model)]="model" [columns]="mockListColumns" [dataModel]="{ name: '', value: '', description: '' }">
<ng-template cell="url" let-scope="scope" let-index="index">
<span>{{ scope.url }}</span>
</ng-template>
</eo-table>

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModalService } from '../../../../shared/services/modal.service';
import { ApiDetailService } from '../api-detail.service';
import { ApiDetailHeaderComponent } from './api-detail-mock.component';
describe('ApiDetailHeaderComponent', () => {
let component: ApiDetailHeaderComponent;
let fixture: ComponentFixture<ApiDetailHeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [ApiDetailService, { provide: ModalService, useValue: {} }],
declarations: [ApiDetailHeaderComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ApiDetailHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ApiEditMock } from 'eo/platform/browser/IndexedDB';
import { ApiDetailService } from '../api-detail.service';
@Component({
selector: 'eo-api-detail-mock',
templateUrl: './api-detail-mock.component.html',
styleUrls: ['./api-detail-mock.component.scss'],
})
export class ApiDetailMockComponent implements OnChanges {
@Input() model: ApiEditMock[] = [];
listConf: object = {};
isVisible = false;
mockListColumns = [
{ title: '名称', key: 'name' },
{ title: 'URL', slot: 'url' },
];
constructor(private detailService: ApiDetailService) {}
ngOnChanges(changes) {
// if (changes.model&&!changes.model.previousValue&&changes.model.currentValue) {
// this.model.push(Object.assign({}, this.itemStructure));
// }
}
handleEditMockItem(index: number) {}
handleDeleteMockItem(index: number) {}
}

View File

@ -1,5 +1,5 @@
<eo-message></eo-message>
<div class="!pt-0 p15">
<nz-collapse class="!pt-0 p15">
<div class="sticky top-0 z-10 pt-6 bg-white">
<button type="submit" nz-button nztype="primary" class="eo_theme_btn_success" (click)="saveApi()">保存</button>
<nz-divider></nz-divider>
@ -122,4 +122,8 @@
</nz-tabset>
</nz-collapse-panel>
</nz-collapse>
</div>
<!-- mock -->
<nz-collapse class="eo_collapse mt40" [nzGhost]="true" *ngIf="electron.isElectron">
<p class="api_line">MOCK <a class="text-xs text-blue-500" (click)="openAddMockModal()">+添加</a></p>
<eo-api-edit-mock [model]="apiData.mockList"></eo-api-edit-mock>
</nz-collapse>

View File

@ -30,6 +30,10 @@ import {
getExpandGroupByKey,
} from '../../../utils/tree/tree.utils';
import { ApiParamsNumPipe } from '../../../shared/pipes/api-param-num.pipe';
import { tree2obj } from '../../../utils/tree/tree.utils';
import { ApiEditMockComponent } from './mock/api-edit-mock.component';
import { ElectronService } from 'eo/workbench/browser/src/app/core/services/electron/electron.service';
@Component({
selector: 'eo-api-edit-edit',
templateUrl: './api-edit.component.html',
@ -37,6 +41,7 @@ import { ApiParamsNumPipe } from '../../../shared/pipes/api-param-num.pipe';
})
export class ApiEditComponent implements OnInit, OnDestroy {
@ViewChild('apiGroup') apiGroup: NzTreeSelectComponent;
@ViewChild(ApiEditMockComponent) apiEditMockComp: ApiEditMockComponent;
validateForm!: FormGroup;
apiData: ApiData;
groups: any[];
@ -53,7 +58,8 @@ export class ApiEditComponent implements OnInit, OnDestroy {
private message: EoMessageService,
private messageService: MessageService,
private apiTab: ApiTabService,
private storage: StorageService
private storage: StorageService,
public electron: ElectronService
) {}
getApiGroup() {
this.groups = [];
@ -88,12 +94,26 @@ export class ApiEditComponent implements OnInit, OnDestroy {
getApi(id) {
this.storage.run('apiDataLoad', [id], (result: StorageHandleResult) => {
if (result.status === StorageHandleStatus.success) {
this.apiData = result.data;
// 如果没有mock则生成系统默认mock
if ((window.eo?.getMockUrl && !Array.isArray(this.apiData.mockList)) || this.apiData.mockList?.length === 0) {
const url = new URL(this.apiData.uri, window.eo.getMockUrl());
this.apiData.mockList = [
{
name: '系统默认期望',
url: url.toString(),
response: JSON.stringify(tree2obj([].concat(this.apiData.responseBody))),
isDefault: true,
},
];
}
['requestBody', 'responseBody'].forEach((tableName) => {
if (['xml', 'json'].includes(result.data[`${tableName}Type`])) {
result.data[tableName] = treeToListHasLevel(result.data[tableName]);
}
});
this.apiData = result.data;
this.changeGroupID$.next(this.apiData.groupID);
this.validateForm.patchValue(this.apiData);
}
@ -112,19 +132,26 @@ export class ApiEditComponent implements OnInit, OnDestroy {
}
const formData: any = Object.assign({}, this.apiData, this.validateForm.value);
formData.groupID = Number(formData.groupID === '-1' ? '0' : formData.groupID);
['requestBody', 'queryParams', 'restParams', 'requestHeaders', 'responseHeaders', 'responseBody'].forEach(
(tableName) => {
if (typeof this.apiData[tableName] !== 'object') {
return;
}
formData[tableName] = this.apiData[tableName].filter((val) => val.name);
if (['requestBody', 'responseBody'].includes(tableName)) {
if (['xml', 'json'].includes(formData[`${tableName}Type`])) {
formData[tableName] = listToTreeHasLevel(formData[tableName]);
}
[
'requestBody',
'queryParams',
'restParams',
'requestHeaders',
'responseHeaders',
'responseBody',
'mockList',
].forEach((tableName) => {
if (typeof this.apiData[tableName] !== 'object') {
return;
}
formData[tableName] = this.apiData[tableName].filter((val) => val.name);
if (['requestBody', 'responseBody'].includes(tableName)) {
if (['xml', 'json'].includes(formData[`${tableName}Type`])) {
formData[tableName] = listToTreeHasLevel(formData[tableName]);
}
}
);
});
this.editApi(formData);
}
bindGetApiParamNum(params) {
@ -142,6 +169,13 @@ export class ApiEditComponent implements OnInit, OnDestroy {
this.destroy$.next();
this.destroy$.complete();
}
/**
* mock弹窗
*/
openAddMockModal() {
this.apiEditMockComp.openAddModal();
}
private initApi(id) {
this.resetForm();
this.initBasicForm();
@ -149,7 +183,6 @@ export class ApiEditComponent implements OnInit, OnDestroy {
if (this.apiTab.currentTab && this.apiTab.tabCache[this.apiTab.tabID]) {
let tabData = this.apiTab.tabCache[this.apiTab.tabID];
this.apiData = tabData.apiData;
this.validateForm.patchValue(this.apiData);
return;
}
if (!id) {

View File

@ -20,6 +20,7 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzDividerModule } from 'ng-zorro-antd/divider';
import { NzAffixModule } from 'ng-zorro-antd/affix';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { ApiEditComponent } from './api-edit.component';
import { ApiEditHeaderComponent } from './header/api-edit-header.component';
@ -27,6 +28,7 @@ import { ApiEditBodyComponent } from './body/api-edit-body.component';
import { ApiEditQueryComponent } from './query/api-edit-query.component';
import { ApiEditRestComponent } from './rest/api-edit-rest.component';
import { ApiParamsExtraSettingComponent } from './extra-setting/api-params-extra-setting.component';
import { ApiEditMockComponent } from './mock/api-edit-mock.component';
import { ApiEditService } from './api-edit.service';
@ -44,6 +46,7 @@ const NZ_COMPONETS = [
NzRadioModule,
NzDividerModule,
NzAffixModule,
NzPopconfirmModule,
];
const COMPONENTS = [
ApiEditComponent,
@ -52,6 +55,7 @@ const COMPONENTS = [
ApiEditQueryComponent,
ApiEditRestComponent,
ApiParamsExtraSettingComponent,
ApiEditMockComponent,
];
@NgModule({
declarations: [...COMPONENTS],
@ -65,6 +69,6 @@ const COMPONENTS = [
SharedModule,
ParamsImportModule,
],
providers:[ApiEditService]
providers: [ApiEditService],
})
export class ApiEditModule {}

View File

@ -0,0 +1,38 @@
<eo-table [(model)]="mocklList" [columns]="mockListColumns" [dataModel]="{ name: '', value: '', description: '' }">
<ng-template cell="url" let-scope="scope" let-index="index">
<span>{{ scope.url }}</span>
</ng-template>
<ng-template cell="action" let-scope="scope" let-index="index">
<div class="flex justify-evenly">
<a nz-button nzType="link" *ngIf="scope.name || scope.url" (click)="handleEditMockItem(index)">编辑</a>
<a nz-button nzType="link" *ngIf="scope.name || scope.url" nz-popconfirm nzPopconfirmTitle="您确定要删除此mock吗?"
nzPopconfirmPlacement="topRight" (nzOnConfirm)="handleDeleteMockItem(index)" [disabled]="scope.isDefault">删除</a>
</div>
</ng-template>
</eo-table>
<nz-modal [(nzVisible)]="isVisible" nzWidth="70%" nzTitle="{{this.currentEditMockIndex === -1 ? '添加' : '编辑' }}mock">
<section *nzModalContent class="flex">
<div class="main-content">
<form nz-form nzLayout="vertical">
<nz-form-item>
<nz-form-label nzFor="currentEditMock.name">mock名称</nz-form-label>
<nz-form-control>
<input nz-input name="name" type="text" [(ngModel)]="currentEditMock.name" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFor="currentEditMock.response">返回值</nz-form-label>
<nz-form-control>
<eo-editor [(code)]="currentEditMock.response" (codeChange)="rawDataChange()"
[eventList]="['type', 'format', 'copy', 'download', 'newTab', 'search', 'replace']"></eo-editor>
</nz-form-control>
</nz-form-item>
</form>
</div>
</section>
<div *nzModalFooter class="footer">
<button nz-button nzType="primary" (click)="handleSave()">保存</button>
<button nz-button nzType="default" (click)="handleCancel()">取消</button>
</div>
</nz-modal>

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModalService } from '../../../../shared/services/modal.service';
import { ApiDetailService } from '../api-detail.service';
import { ApiDetailHeaderComponent } from './api-detail-mock.component';
describe('ApiDetailHeaderComponent', () => {
let component: ApiDetailHeaderComponent;
let fixture: ComponentFixture<ApiDetailHeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [ApiDetailService, { provide: ModalService, useValue: {} }],
declarations: [ApiDetailHeaderComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ApiDetailHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,84 @@
import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
import { ApiEditMock } from 'eo/platform/browser/IndexedDB';
import { Subject } from 'rxjs';
import { takeUntil, debounceTime } from 'rxjs/operators';
@Component({
selector: 'eo-api-edit-mock',
templateUrl: './api-edit-mock.component.html',
styleUrls: ['./api-edit-mock.component.scss'],
})
export class ApiEditMockComponent implements OnChanges {
@Input() model: ApiEditMock[] = [];
@Output() modelChange: EventEmitter<any> = new EventEmitter();
isVisible = false;
mockUrl = window.eo?.getMockUrl?.();
private $mocklList: ApiEditMock[] = [];
get mocklList() {
return this.$mocklList;
}
set mocklList(_) {
this.$mocklList = this.model.map((item) => {
const { pathname, search } = new URL(item.url, this.mockUrl);
item.url = `${new URL(pathname + search, this.mockUrl)}`;
return item;
});
}
mockListColumns = [
{ title: '名称', key: 'name' },
{ title: 'URL', slot: 'url' },
{ title: '', slot: 'action', width: '15%' },
];
/** 当前被编辑的mock */
currentEditMock: ApiEditMock;
/** 当前被编辑的mock索引 */
currentEditMockIndex = -1;
private destroy$: Subject<void> = new Subject<void>();
private rawChange$: Subject<string> = new Subject<string>();
constructor() {
this.rawChange$.pipe(debounceTime(700), takeUntil(this.destroy$)).subscribe(() => {});
this.mocklList = [...this.model];
}
ngOnChanges(changes: SimpleChanges): void {
const { model } = changes;
if (model.currentValue) {
this.mocklList = [...this.model];
}
}
rawDataChange() {
this.rawChange$.next(this.currentEditMock.response);
}
handleEditMockItem(index: number) {
this.currentEditMock = { ...this.model[index] };
this.currentEditMockIndex = index;
this.isVisible = true;
}
handleDeleteMockItem(index: number) {
this.model.splice(index, 1);
this.mocklList = [...this.model];
}
handleSave() {
this.isVisible = false;
this.currentEditMockIndex === -1
? this.model.push(this.currentEditMock)
: (this.model[this.currentEditMockIndex] = this.currentEditMock);
this.mocklList = [...this.model];
this.modelChange.emit([...this.model]);
}
handleCancel() {
this.isVisible = false;
}
openAddModal() {
this.currentEditMockIndex = -1;
this.isVisible = true;
this.currentEditMock = {
url: this.model.at(0).url,
name: '',
response: '',
};
}
}

View File

@ -13,6 +13,7 @@ import { NzTreeComponent } from 'ng-zorro-antd/tree';
import { ModalService } from '../../../../shared/services/modal.service';
import { StorageService } from '../../../../shared/services/storage';
import { ElectronService } from '../../../../core/services';
import { tree2obj } from '../../../../utils/tree/tree.utils';
@Component({
selector: 'eo-api-group-tree',
templateUrl: './api-group-tree.component.html',
@ -114,11 +115,16 @@ export class ApiGroupTreeComponent implements OnInit, OnDestroy {
});
}
getApis() {
// 注册mock路由
const registerMockRoute = window.eo?.registerMockRoute;
// 重置并初始化路由
window.eo?.resetAndInitRoutes?.();
this.storage.run('apiDataLoadAllByProjectID', [this.projectID], (result: StorageHandleResult) => {
const { success, empty } = StorageHandleStatus;
if ([success, empty].includes(result.status)) {
let apiItems = {};
result.data.forEach((item) => {
result.data.forEach((item: ApiData) => {
delete item.updatedAt;
apiItems[item.uuid] = item;
this.treeItems.push({
@ -129,6 +135,21 @@ export class ApiGroupTreeComponent implements OnInit, OnDestroy {
method: item.method,
isLeaf: true,
});
if (this.electron.isElectron && registerMockRoute) {
if (Array.isArray(item.mockList) && item.mockList.length > 0) {
item.mockList.forEach((n) => {
registerMockRoute({ method: item.method, path: n.url, data: n.response });
});
} else {
registerMockRoute({
method: item.method,
path: item.uri,
data: tree2obj(item.responseBody as any[]),
});
}
// console.log('registerMockRoute', { method: item.method, path: item.uri, data: tree2obj(item.responseBody) });
}
});
this.apiDataItems = apiItems;
this.messageService.send({ type: 'loadApi', data: this.apiDataItems });
@ -136,6 +157,7 @@ export class ApiGroupTreeComponent implements OnInit, OnDestroy {
this.generateGroupTreeData();
this.restoreExpandStatus();
}
console.log('result', result.data);
});
}
restoreExpandStatus() {

View File

@ -2,7 +2,7 @@
(nzSelectChange)="pickTab()" [nzTabBarExtraContent]="extraTemplate">
<nz-tab *ngFor="let tab of tabSerive.tabs; let i = index" nzClosable [nzTitle]="titleTemplate">
<ng-template #titleTemplate>
<span class="mr5 method_text method_text_{{ tab.method }}" *ngIf="tab.method">{{ tab.method }}</span>
<span class="mr5 method_text_{{ tab.method }}" *ngIf="tab.method">{{ tab.method }}</span>
<span class="text_omit tab_text"> {{ tab.title }}</span>
</ng-template>
</nz-tab>

View File

@ -90,7 +90,7 @@ export class ApiTestComponent implements OnInit, OnDestroy {
*/
restoreHistory(item) {
let result = this.apiTest.getTestDataFromHistory(item);
console.log('restoreHistory',result)
console.log('restoreHistory', result);
//restore request
this.apiData = result.testData;
this.changeUri();
@ -173,7 +173,7 @@ export class ApiTestComponent implements OnInit, OnDestroy {
* Receive Test Server Message
*/
private receiveMessage(message) {
console.log('receiveMessage',message);
console.log('receiveMessage', message);
let tmpHistory = {
general: message.general,
request: message.report.request,

View File

@ -152,7 +152,7 @@ export class SettingComponent implements OnInit {
*
*/
private init() {
if (!window.eo && !window.eo.getFeature) return;
if (!window.eo && !window.eo?.getFeature) return;
this.isVisible = true;
this.settings = {};
this.nestedSettings = {};

View File

@ -1,4 +1,11 @@
import { GroupTreeItem } from '../../shared/models';
export type TreeToObjOpts = {
key?: string;
valueKey?: string;
childKey?: string;
};
/**
* Convert old component listBlock array items has level without parent id to tree nodes
* @param list Array<GroupTreeItem>
@ -13,7 +20,7 @@ export const listToTreeHasLevel = (
) => {
const listDepths = [];
//delete useless key
const uselessKeys = ['listDepth', 'isHide','isShrink'];
const uselessKeys = ['listDepth', 'isHide', 'isShrink'];
list = list.map((item) => {
listDepths.push(item.listDepth);
return Object.keys(item).reduce(
@ -146,3 +153,22 @@ export const getExpandGroupByKey = (component, key) => {
}
return expandKeys;
};
/**
* key => value
*
* @param list
* @param opts
* @returns
*/
export const tree2obj = (list: any[], opts: TreeToObjOpts = {}, initObj = {}) => {
const { key = 'name', valueKey = 'description', childKey = 'children' } = opts;
return list.reduce((prev, curr) => {
prev[curr[key]] = curr[valueKey];
if (Array.isArray(curr[childKey]) && curr[childKey].length > 0) {
tree2obj(curr[childKey], opts, (prev[curr[key]] = {}));
}
return prev;
}, initObj);
};

View File

@ -1,12 +1,18 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"module": "commonjs",
"outDir": "./out",
"rootDir": "./src",
"target": "es5",
"types": [
"node"
@ -25,12 +31,15 @@
}
},
"include": [
"**/**.ts",
"./src/**/**.ts",
"./src/**/**.js",
],
"exclude": [
"node_modules",
"**/*.spec.ts",
"**/browser/**/*.ts"
"**/browser/**/*.ts",
"**/browser/**/*.js",
"out"
],
"angularCompilerOptions": {
"enableIvy": true

4520
yarn.lock

File diff suppressed because it is too large Load Diff