API test support send file and response preview file/image (#74)

* fix: extension overflow page not show

* feat: api test support send file

* feat: response support file

Co-authored-by: 夜鹰 <17kungfuboy@gmail.com>
This commit is contained in:
Scarqin 2022-06-17 19:17:39 +08:00 committed by GitHub
parent 69753f1f1f
commit f83a674b1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 172 additions and 241 deletions

View File

@ -1,6 +1,6 @@
{
"name": "eoapi",
"version": "1.1.1",
"version": "1.2.0-beta",
"main": "out/app/electron-main/main.js",
"description": "A lightweight, extensible API tool",
"homepage": "https://github.com/eolinker/eoapi.git",
@ -68,4 +68,4 @@
"node-module-alias": {
"eo": "./out"
}
}
}

View File

@ -12,7 +12,6 @@
</span>
</div>
<ace
style="height: 7em; width: 100%"
[config]="config"
[disabled]="false"
[(value)]="code"

View File

@ -79,6 +79,9 @@ export class EoEditorComponent implements AfterViewInit, OnInit, OnChanges {
theme: 'tomorrow_night_eighties',
readOnly: false,
tabSize: 4,
minLines:5,
maxLines: 20
};
constructor(private message: EoMessageService, private electron: ElectronService) {}

View File

@ -36,18 +36,6 @@ nz-sider {
}
}
eo-api {
router-outlet + * {
display: block;
height: calc(100vh - var(--NAVBAR_HEIGHT) - var(--FOOTER_HEIGHT) - var(--remote-notification-height) - 46px);
overflow: auto;
}
.has_tab_page router-outlet + * {
height: calc(
100vh - var(--NAVBAR_HEIGHT) - var(--FOOTER_HEIGHT) - var(--remote-notification-height) - 45px - 47px
);
}
}
.method_text_GET {
color: var(--BLUE_TAG_BG);
}

View File

@ -111,7 +111,6 @@ export class ApiGroupEditComponent implements OnInit {
this.modalRef.destroy();
this.storage.run('groupBulkRemove', [data.group], (result: StorageRes) => {
if (result.status === StorageResStatus.success) {
this.messageService.send({ type: 'updateGroupSuccess', data: {} });
//delete group api
if (data.api.length > 0) {
this.messageService.send({ type: 'gotoBulkDeleteApi', data: { uuids: data.api } });

View File

@ -4,7 +4,6 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Select } from '@ngxs/store';
import {
ApiData,
RequestMethod,
RequestProtocol,
StorageRes,

View File

@ -81,6 +81,7 @@ export class ApiTestService {
baseFun: {
reduceItemWhenAddChildItem: reduceItemWhenIsOprDepth,
watchCheckboxChange: opts.watchFormLastChange,
importFile: opts.importFile
},
itemStructure: Object.assign({}, opts.itemStructure),
tdList: [
@ -114,8 +115,10 @@ export class ApiTestService {
{
thKey: '参数值',
type: 'input',
type: 'autoCompleteAndFile',
modelKey: 'value',
switchVar: 'type',
swicthFile: 'file',
placeholder: '参数值',
width: 300,
mark: 'value',
@ -273,7 +276,6 @@ export class ApiTestService {
});
if (inData.history.response.responseType === 'text') {
let bodyInfo = text2UiData(inData.history.response.body);
console.log(bodyInfo);
result.responseBody = bodyInfo.data;
result.responseBodyType = bodyInfo.textType;
result.responseBodyJsonType = bodyInfo.rootType;
@ -282,7 +284,7 @@ export class ApiTestService {
}
getTestDataFromApi(inData) {
let editToTestParams = (arr) => {
arr=arr||[];
arr = arr || [];
arr.forEach((val) => {
val.value = val.example;
delete val.example;
@ -334,7 +336,7 @@ export class ApiTestService {
case 'formData': {
inData.requestBody.forEach((val) => {
val.value = val.example;
val.type = 'string';
val.type = val.type === 'file' ? 'file' : 'string';
delete val.example;
});
break;

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, OnDestroy } from '@angular/core';
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Subject } from 'rxjs';
import { pairwise, takeUntil, debounceTime } from 'rxjs/operators';
@ -10,7 +10,7 @@ import {
} from '../../../../shared/services/api-test/api-test-params.model';
import { ApiBodyType, JsonRootType } from '../../../../shared/services/storage/index.model';
import { ApiTestService } from '../api-test.service';
import { Message, MessageService } from '../../../../shared/services/message';
import { EoMessageService } from 'eo/workbench/browser/src/app/eoui/message/eo-message.service';
@Component({
selector: 'eo-api-test-body',
@ -40,7 +40,11 @@ export class ApiTestBodyComponent implements OnInit, OnChanges, OnDestroy {
private bodyType$: Subject<string> = new Subject<string>();
private destroy$: Subject<void> = new Subject<void>();
private rawChange$: Subject<string> = new Subject<string>();
constructor(private apiTest: ApiTestService, private messageService: MessageService) {
constructor(
private apiTest: ApiTestService,
private cdRef: ChangeDetectorRef,
private message: EoMessageService
) {
this.bodyType$.pipe(pairwise(), takeUntil(this.destroy$)).subscribe((val) => {
this.beforeChangeBodyByType(val[0]);
});
@ -168,6 +172,33 @@ export class ApiTestBodyComponent implements OnInit, OnChanges, OnDestroy {
watchFormLastChange: () => {
this.modelChange.emit(this.model);
},
importFile: (inputArg) => {
if (inputArg.file.length === 0) return;
inputArg.item.value = '';
inputArg.item.files = [];
for (var i = 0; i < inputArg.file.length; i++) {
var val = inputArg.file[i];
if (val.size > 2 * 1024 * 1024) {
inputArg.item.value = '';
this.message.error('文件大小均需小于2M');
return;
}
}
for (var i = 0; i < inputArg.file.length; i++) {
var val = inputArg.file[i];
inputArg.item.value = val.name + ',' + inputArg.item.value;
let tmpReader = new FileReader();
tmpReader.readAsDataURL(val);
tmpReader.onload = function (_default) {
inputArg.item.files.splice(0, 0, {
name: val.name,
dataUrl: this.result,
});
};
}
inputArg.item.value = inputArg.item.value.slice(0, inputArg.item.value.length - 1);
this.modelChange.emit(this.model);
},
});
this.cache['listConfSetting'] = Object.assign({}, this.listConf.setting);
}

View File

@ -5,7 +5,7 @@
</ng-template>
</nz-empty>
</div>
<div class="p15" *ngIf="model.responseType">
<div class="p15" *ngIf="model.responseType" [ngSwitch]="model.responseType">
<div class="mb15 basic_info_bar cw f_row f_js {{ model.statusCode ? codeStatus.class : 'code_red' }}">
<div class="fs16" id="statusCode">{{ model.statusCode || 'No Response' }}</div>
<div class="f_row_ac fs12">
@ -13,7 +13,25 @@
<span id="time">Time: {{ model.testDeny }}ms</span>
</div>
</div>
<div class="text-center" *ngSwitchCase="'stream'">
<div *ngIf="!responseIsImg">
无法预览非文本类型的数据,您可以
<button class="eo_theme_btn_default mlr5" type="button" (click)="downloadResponseText()">下载返回结果</button>
,并用其他程序打开。
</div>
<!-- <div class="mt20" *ngIf="responseIsImg">
<img class="maw_100percent" [src]="model.blobUrl" />
</div> -->
</div>
<div class="text-center" *ngSwitchCase="'longText'">
响应结果超出可预览的大小,您可以
<button class="eo_theme_btn_default mlr5" type="button" (click)="downloadResponseText()">下载返回结果</button>
<!-- 或者
<button class="eo_theme_btn_default" type="button" (click)="newTabResponseText()">在新标签页中显示返回结果</button>
并用其他程序打开。 -->
</div>
<eo-editor
*ngSwitchDefault
class="mt20"
[autoFormat]="true"
[(code)]="model.body"

View File

@ -1,4 +1,5 @@
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { getBlobUrl } from 'eo/workbench/browser/src/app/utils';
import { ApiTestHistoryResponse } from '../../../../shared/services/storage/index.model';
import { ApiTestService } from '../api-test.service';
@Component({
@ -10,12 +11,33 @@ export class ApiTestResultResponseComponent implements OnInit, OnChanges {
@Input() model: any | ApiTestHistoryResponse;
codeStatus: { status: string; cap: number; class: string };
size: string;
blobUrl:string='';
responseIsImg = false;
constructor(private apiTest: ApiTestService) {}
ngOnChanges(changes) {
if (changes.model) {
this.codeStatus = this.apiTest.getHTTPStatus(this.model.statusCode);
//show response
// this.responseIsImg =/\.((jpg)|(jpeg)|(png)|(gif)|(bmg))/i.test(this.model.blobFileName) || /image/.test(this.model.contentType);
}
}
ngOnInit(): void {}
downloadResponseText() {
this.blobUrl=getBlobUrl(this.model.body, this.model.contentType);
const blobFileName = decodeURI(this.model.blobFileName);
const tmpAElem = document.createElement('a');
if ('download' in tmpAElem) {
tmpAElem.style.visibility = 'hidden';
tmpAElem.href = this.blobUrl;
tmpAElem.download = blobFileName;
document.body.appendChild(tmpAElem);
const evt = document.createEvent('MouseEvents');
evt.initEvent('click', true, true);
tmpAElem.dispatchEvent(evt);
document.body.removeChild(tmpAElem);
} else {
location.href = this.blobUrl;
}
}
newTabResponseText() {}
}

View File

@ -1,66 +1,3 @@
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.w-40 {
width: 10rem;
}
.mr-8 {
margin-right: 2rem;
}
.h-40 {
height: 10rem;
}
.block {
display: block;
}
.border {
border-width: 1px;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-cover {
background-size: cover;
}
.bg-no-repeat {
background-repeat: no-repeat;
}
.bg-center {
background-position: center;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.font-bold {
font-weight: 700;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-col {
flex-direction: column;
}
.flex {
display: flex;
}
.w-full {
width: 100%;
}
.h-20 {
height: 5rem;
}
.p-8 {
padding: 2rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
::ng-deep {
eo-extension-detail .ant-tabs-content-holder {
padding-top: 20px;

View File

@ -10,7 +10,7 @@
{{ item.title }}<span *ngIf="item.showNum"> {{ extensionService.extensionIDs.length }}</span>
</div>
</section>
<section class="right fg1 px-4">
<section class="right fg1 pl-4">
<router-outlet></router-outlet>
</section>
</section>

View File

@ -39,7 +39,6 @@
margin-right: 0.5rem;
}
}
.px-4 {
.pl-4 {
padding-left: 1rem;
padding-right: 1rem;
}

View File

@ -18,147 +18,14 @@
.warn-color {
color: var(--MAIN_THEME_COLOR);
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.flex-1 {
flex: 1 1 0%;
}
.w-60 {
width: 15rem;
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.gap-6 {
grid-gap: 1.5rem;
gap: 1.5rem;
}
.py-5 {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.grid {
display: grid;
}
@media (max-width: 1279.9px) {
.grid-cols-4 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.w-full {
width: 100%;
.w-60{
width: 15rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.h-76 {
.h-76{
height: 17rem;
}
.items-center {
align-items: center;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-col {
flex-direction: column;
}
.flex {
display: flex;
}
.border {
border-width: 1px;
}
.rounded-lg {
border-radius: 0.5rem;
}
.text-green-700 {
--tw-text-opacity: 1;
color: rgba(4, 120, 87, var(--tw-text-opacity));
}
.p-1 {
padding: 0.25rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.border {
border-width: 1px;
}
.rounded-sm {
border-radius: 0.125rem;
}
.border-green-700 {
--tw-border-opacity: 1;
border-color: rgba(4, 120, 87, var(--tw-border-opacity));
}
.w-20 {
width: 5rem;
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.h-20 {
height: 5rem;
}
.block {
display: block;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-cover {
background-size: cover;
}
.bg-no-repeat {
background-repeat: no-repeat;
}
.bg-center {
background-position: center;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-bold {
font-weight: 700;
}
.text-gray-400 {
--tw-text-opacity: 1;
color: rgba(156, 163, 175, var(--tw-text-opacity));
}
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.text-gray-500 {
--tw-text-opacity: 1;
color: rgba(107, 114, 128, var(--tw-text-opacity));
}
.my-1 {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}

View File

@ -1,4 +1,4 @@
.home_container{
.home_container {
height: calc(100vh - var(--NAVBAR_HEIGHT) - var(--remote-notification-height, 0px) - 1px);
}
.home {
@ -14,3 +14,24 @@
background-color: rgb(255, 219, 7);
overflow: hidden;
}
::ng-deep {
eo-api {
router-outlet + * {
display: block;
height: calc(100vh - var(--NAVBAR_HEIGHT) - var(--FOOTER_HEIGHT) - var(--remote-notification-height) - 46px);
overflow: auto;
}
.has_tab_page router-outlet + * {
height: calc(
100vh - var(--NAVBAR_HEIGHT) - var(--FOOTER_HEIGHT) - var(--remote-notification-height) - 45px - 47px
);
}
}
eo-extension {
router-outlet + * {
display: block;
height: calc(100vh - var(--NAVBAR_HEIGHT) - var(--FOOTER_HEIGHT) - var(--remote-notification-height));
overflow: auto;
}
}
}

View File

@ -1,6 +1,6 @@
export enum ApiTestParamsTypeFormData {
text = 'string',
// file = 'file',
file = 'file',
}
export enum ApiTestParamsTypeJsonOrXml {
string = 'string',
@ -32,6 +32,10 @@ export interface ApiTestBody extends BasiApiTestParams {
* param type
*/
type: string;
/**
* If value is file,value is base64 string
*/
files?:string;
/**
* XML attribute
*/

View File

@ -58,6 +58,7 @@ export const eoFormatRequestData = (data, opts = { env: {} }, locale) => {
checkbox: val.required,
listDepth: val.listDepth || 0,
paramKey: val.name,
files:val.files?.map(val=>val.dataUrl),
paramType: typeMUI[val.type],
paramInfo: val.value === undefined ? val.example : val.value,
});
@ -100,6 +101,7 @@ export const eoFormatRequestData = (data, opts = { env: {} }, locale) => {
};
export const eoFormatResponseData = ({ report, history, id }) => {
let { httpCode, ...response } = history.resultInfo;
console.log(report, history, id)
response = {
statusCode: httpCode,
...response,
@ -115,7 +117,7 @@ export const eoFormatResponseData = ({ report, history, id }) => {
} = {
id: id,
general: report.general,
response: response,
response: {blobFileName:report.blobFileName,...response},
report: {
request: {
requestHeaders: report.request.headers.map((val) => ({ name: val.key, value: val.value })),

View File

@ -42,6 +42,9 @@ export interface TestLocalNodeData {
checkbox: boolean;
paramKey: string;
paramType: string;
/**
* value
*/
paramInfo: string;
childList: object[];
}[];

View File

@ -110,6 +110,7 @@ export interface ApiTestHistoryResponse {
body: string;
contentType: string;
responseType: 'text' | 'longText' | 'stream';
blobFileName?:string;
responseLength: number;
testDeny: string;
/**
@ -295,51 +296,51 @@ export enum RequestProtocol {
}
/**
* API数据对象接口
* API Data
*/
export interface ApiData extends StorageModel {
/**
*
* name
* @type {string}
*/
name: string;
/**
* ID
* Belongs to which project
*
* @type {string|number}
*/
projectID?: string | number;
/**
* ID
* Belongs to which group
*
* @type {string|number}
*/
groupID: string | number;
/**
*
* Request url,Usually value is path
*
* @type {string}
*/
uri: string;
/**
* API协议 [http, https, ...]
* API protocol [http, https, ...]
*
* @type {RequestProtocol|string}
*/
protocol: RequestProtocol | string;
/**
* [POST, GET, PUT, ...]
* Request method [POST, GET, PUT, ...]
*
* @type {RequestMethod|string}
*/
method: RequestMethod | string;
/**
*
* api show order
*
* @type {number}
*/

View File

@ -93,3 +93,38 @@ export const getDefaultValue = (list: any[], key) => {
const [target] = list.filter((it) => it.default);
return target[key] || '';
};
export const parserProperties = (properties) => Object.keys(properties).map((it) => ({ value: it, ...properties[it] }));
const base64ToUint8Array = (inputBase64String) => {
const tmpPadding = '='.repeat((4 - (inputBase64String.length % 4)) % 4);
const tmpBase64 = (inputBase64String + tmpPadding).replace(/\-/g, '+').replace(/_/g, '/');
const tmpRawData = window.atob(tmpBase64);
const tmpOutputArray = new Uint8Array(tmpRawData.length);
for (let i = 0; i < tmpRawData.length; ++i) {
tmpOutputArray[i] = tmpRawData.charCodeAt(i);
}
return tmpOutputArray;
};
export const getBlobUrl = (inputStream, inputFileType) => {
let tmpBlob;
try {
inputStream = base64ToUint8Array(inputStream);
if (typeof window.Blob === 'function') {
tmpBlob = new Blob([inputStream], {
type: inputFileType,
});
} else {
//@ts-ignore
const tmpBlobBuilder =
window.BlobBuilder || window.MozBlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder;
const tmpBlobClass = new tmpBlobBuilder();
tmpBlobClass.append(inputStream);
tmpBlob = tmpBlobClass.getBlob(inputFileType);
}
} catch (GET_BLOB_ERR) {
tmpBlob = inputStream;
}
const tmpUrlObj = window.URL || window.webkitURL;
return tmpUrlObj.createObjectURL(tmpBlob);
};

View File

@ -1252,8 +1252,10 @@ function listBlockController($rootScope, $element, $scope) {
};
$scope.importFile = function (inputArg, inputEvent) {
inputArg.$index = this.$parent.$index;
inputArg.item=vm.list[inputArg.$index];
vm.mainObject.baseFun.importFile(inputArg);
if (inputEvent) inputEvent.value = '';
// if (inputEvent) inputEvent.value = '';
($scope.$root && $scope.$root.$$phase) || $scope.$apply();
};
/**
* @desc 过滤列表函数

View File

@ -88,7 +88,6 @@ function selectDefaultController($scope, $element) {
if (vm.mainObject && vm.mainObject.isNeedToResetPosition) {
let tmpObj = vm.data.inputElem[0].getBoundingClientRect();
console.log(tmpObj);
vm.data.inputX = tmpObj.x;
vm.data.inputY = tmpObj.y + 30;
}