Skip to content

Commit da58c67

Browse files
(v2.6) File uploads (single and multiple)
1 parent 9d52c84 commit da58c67

File tree

7 files changed

+144
-72
lines changed

7 files changed

+144
-72
lines changed

ui/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ npm install
88
npm run dev
99
```
1010

11-
**Open in Browser** http://localhost:3000?api=http://localhost:3000/sample.json
11+
**Open in Browser** http://localhost:3000/request-docs?api=http://localhost:3000/request-docs/sample.json
1212

1313

1414
### Developing with Laravel
1515

1616
#### Step 1
1717

18-
**Optional** Enable CORS on Laravel to allow localhost:3000
18+
**Optional** Enable CORS on Laravel to allow localhost:3000/request-docs
1919
**Recommended** Open Chrome with `--disable-web-security` flag
2020

2121
On Mac to open chrome command:

ui/src/components/ApiAction.tsx

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default function ApiAction(props: Props) {
2828
const [sendingRequest, setSendingRequest] = useState(false);
2929
const [queryParams, setQueryParams] = useState('');
3030
const [bodyParams, setBodyParams] = useState('');
31+
const [fileParams, setFileParams] = useState(null);
3132
const [responseData, setResponseData] = useState("");
3233
const [sqlQueriesCount, setSqlQueriesCount] = useState(0);
3334
const [sqlData, setSqlData] = useState("");
@@ -37,13 +38,20 @@ export default function ApiAction(props: Props) {
3738
const [responseHeaders, setResponseHeaders] = useState("");
3839
const [activeTab, setActiveTab] = useState('info');
3940

40-
const handleFileChange = (files: any) => {
41-
const bodyAppend = JSON.parse(bodyParams)
42-
bodyAppend["avatar"] = files[0]
43-
setBodyParams(JSON.stringify(bodyAppend))
41+
const handleFileChange = (files: any, file: any) => {
42+
const formData: any = new FormData()
43+
if (file.includes('.*')) {
44+
const fileParam = file.replace('.*', '')
45+
for (let i = 0; i < files.length; i++) {
46+
formData.append(`${fileParam}[${i}]`, files[i]);
47+
}
48+
} else {
49+
formData.append(file, files[0])
50+
}
51+
setFileParams(formData)
4452
}
4553

46-
// // update localstorage
54+
// // update localstorage
4755
const updateLocalStorage = () => {
4856
const jsonAllParamsRegistry = JSON.parse(allParamsRegistry)
4957
if (method == 'GET' || method == 'HEAD' || method == 'DELETE') {
@@ -76,11 +84,22 @@ export default function ApiAction(props: Props) {
7684
if (method == 'POST' || method == 'PUT' || method == 'PATCH') {
7785
try {
7886
JSON.parse(bodyParams)
87+
if (fileParams != null) {
88+
for (const [key, value] of Object.entries(JSON.parse(bodyParams))) {
89+
fileParams.append(key, value)
90+
}
91+
}
92+
7993
} catch (error: any) {
8094
setError("Request body incorrect: " + error.message)
8195
return
8296
}
83-
options['body'] = bodyParams
97+
98+
if (fileParams != null) {
99+
options['body'] = fileParams // includes body as well
100+
} else {
101+
options['body'] = bodyParams // just the body
102+
}
84103
}
85104

86105
const startTime = performance.now();
@@ -93,51 +112,51 @@ export default function ApiAction(props: Props) {
93112
setError(null)
94113

95114
fetch(`${host}/${requestUri}${queryParams}`, options)
96-
.then((response) => {
97-
let timeTaken = performance.now() - startTime
98-
// round to 3 decimals
99-
timeTaken = Math.round((timeTaken + Number.EPSILON) * 1000) / 1000
100-
setTimeTaken(timeTaken)
101-
setResponseStatus(response.status)
102-
setResponseHeaders(JSON.stringify(Object.fromEntries(response.headers), null, 2))
103-
setSendingRequest(false)
104-
return response.json();
105-
}).then((data) => {
106-
107-
if (data && data._lrd && data._lrd.queries) {
108-
const sqlQueries = data._lrd.queries.map((query: any) => {
109-
return "Connection: "
110-
+ query.connection_name
111-
+ " Time taken: "
112-
+ query.time
113-
+ "ms: \n"
114-
+ query.sql + "\n"
115-
}).join("\n")
116-
setSqlData(sqlQueries)
117-
setSqlQueriesCount(data._lrd.queries.length)
118-
}
119-
if (data && data._lrd && data._lrd.logs) {
120-
let logs = ""
121-
for (const value of data._lrd.logs) {
122-
logs += value.level + ": " + value.message + "\n"
115+
.then((response) => {
116+
let timeTaken = performance.now() - startTime
117+
// round to 3 decimals
118+
timeTaken = Math.round((timeTaken + Number.EPSILON) * 1000) / 1000
119+
setTimeTaken(timeTaken)
120+
setResponseStatus(response.status)
121+
setResponseHeaders(JSON.stringify(Object.fromEntries(response.headers), null, 2))
122+
setSendingRequest(false)
123+
return response.json();
124+
}).then((data) => {
125+
126+
if (data && data._lrd && data._lrd.queries) {
127+
const sqlQueries = data._lrd.queries.map((query: any) => {
128+
return "Connection: "
129+
+ query.connection_name
130+
+ " Time taken: "
131+
+ query.time
132+
+ "ms: \n"
133+
+ query.sql + "\n"
134+
}).join("\n")
135+
setSqlData(sqlQueries)
136+
setSqlQueriesCount(data._lrd.queries.length)
123137
}
124-
setLogData(logs)
125-
}
126-
if (data && data._lrd && data._lrd.memory) {
127-
setServerMemory(data._lrd.memory)
128-
}
129-
// remove key _lrd from response
130-
if (data && data._lrd) {
131-
delete data._lrd
132-
}
133-
setResponseData(JSON.stringify(data, null, 2))
134-
setActiveTab('response')
135-
}).catch((error) => {
136-
setError("Response error: " + error)
137-
setResponseStatus(500)
138-
setSendingRequest(false)
139-
setActiveTab('response')
140-
})
138+
if (data && data._lrd && data._lrd.logs) {
139+
let logs = ""
140+
for (const value of data._lrd.logs) {
141+
logs += value.level + ": " + value.message + "\n"
142+
}
143+
setLogData(logs)
144+
}
145+
if (data && data._lrd && data._lrd.memory) {
146+
setServerMemory(data._lrd.memory)
147+
}
148+
// remove key _lrd from response
149+
if (data && data._lrd) {
150+
delete data._lrd
151+
}
152+
setResponseData(JSON.stringify(data, null, 2))
153+
setActiveTab('response')
154+
}).catch((error) => {
155+
setError("Response error: " + error)
156+
setResponseStatus(500)
157+
setSendingRequest(false)
158+
setActiveTab('response')
159+
})
141160

142161
}
143162

@@ -213,6 +232,7 @@ export default function ApiAction(props: Props) {
213232
)}
214233
{activeTab == 'request' && (
215234
<ApiActionRequest
235+
lrdDocsItem={lrdDocsItem}
216236
requestUri={requestUri}
217237
method={method}
218238
sendingRequest={sendingRequest}

ui/src/components/elements/ApiActionLog.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default function ApiActionLog(props: Props) {
3030
maxLines={50}
3131
width='100%'
3232
mode="sh"
33+
readOnly={true}
3334
value={logData}
3435
theme="one_dark"
3536
wrapEnabled={true}

ui/src/components/elements/ApiActionRequest.tsx

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import React from 'react';
1+
import React, { useState, useEffect } from 'react';
22

3+
import type { IAPIInfo } from '../libs/types'
4+
import Files from 'react-files'
35
import "ace-builds";
6+
import shortid from 'shortid';
47
import jsonWorkerUrl from 'ace-builds/src-min-noconflict/worker-json?url';
58
ace.config.setModuleUrl('ace/mode/json_worker', jsonWorkerUrl);
69

@@ -9,10 +12,11 @@ import "ace-builds/src-noconflict/mode-json";
912
import "ace-builds/src-noconflict/theme-one_dark";
1013
import "ace-builds/src-noconflict/ext-language_tools";
1114

12-
import { PaperAirplaneIcon } from '@heroicons/react/24/solid'
15+
import { PaperAirplaneIcon, ChevronRightIcon } from '@heroicons/react/24/solid'
1316

1417

1518
interface Props {
19+
lrdDocsItem: IAPIInfo,
1620
requestUri: string,
1721
method: string,
1822
sendingRequest: boolean,
@@ -22,13 +26,15 @@ interface Props {
2226
setRequestUri: (requestUri: string) => void,
2327
handleSendRequest: () => void,
2428
handleChangeRequestHeaders: (requestHeaders: string) => void,
25-
handleFileChange: (e: any) => void,
29+
handleFileChange: (files: any, file: any) => void,
2630
setBodyParams: (bodyParams: string) => void,
2731
setQueryParams: (queryParams: string) => void,
2832
}
2933

3034
export default function ApiActionRequest(props: Props) {
31-
const { requestUri,
35+
const {
36+
lrdDocsItem,
37+
requestUri,
3238
method,
3339
sendingRequest,
3440
requestHeaders,
@@ -41,6 +47,27 @@ export default function ApiActionRequest(props: Props) {
4147
setBodyParams,
4248
setQueryParams } = props
4349

50+
const [files, setFiles] = useState<any>([])
51+
const [uploadedFiles, setUploadedFiles] = useState<any>({})
52+
53+
const handleFileUploaded = (files: any, file: any) => {
54+
const uf = { ...uploadedFiles }
55+
uf[file] = files
56+
setUploadedFiles(uf)
57+
handleFileChange(files, file)
58+
}
59+
60+
useEffect(() => {
61+
//check if lrdDocsItem has rules
62+
const files: any = []
63+
for (const [key, rule] of Object.entries(lrdDocsItem.rules)) {
64+
if (rule.includes('file') || rule.includes('image')) {
65+
files.push(key)
66+
}
67+
}
68+
setFiles(files)
69+
}, [])
70+
4471
return (
4572
<>
4673
<div className="form-control">
@@ -105,18 +132,40 @@ export default function ApiActionRequest(props: Props) {
105132
{(method == 'POST' || method == 'PUT' || method == 'PATCH') && (
106133
<div className="mockup-code">
107134
<span className='pl-5 text-sm text-slate-500'>REQUEST BODY</span>
108-
{/* <div className='pl-5'>Image</div>
109-
<div className="files-dropzone">
110-
<Files
111-
className='files-dropzone'
112-
onChange={handleFileChange}
113-
multiple={true}
114-
maxFileSize={10000000}
115-
minFileSize={0}
116-
clickable>
117-
Drop files here or click to upload
118-
</Files>
119-
</div> */}
135+
{files.map((file: string) =>
136+
<div key={shortid.generate()}>
137+
<div className='m-2 pl-3'>
138+
<code>
139+
<small>{file}</small>
140+
</code>
141+
{file.includes('.*') && (
142+
<ChevronRightIcon className='inline-block w-4 h-4 ml-1' />
143+
)}
144+
</div>
145+
<Files
146+
className='p-5 bg-gray-800 border border-gray-500 border-double hover:bg-gray-700 hover:border-dashed hover:cursor-pointer'
147+
onChange={(e: any) => handleFileUploaded(e, file)}
148+
multiple={file.includes('.*')}
149+
maxFileSize={10000000}
150+
minFileSize={0}
151+
clickable
152+
>
153+
{uploadedFiles[file] && uploadedFiles[file].length > 0 && (
154+
<div className='text-sm text-gray-300'>
155+
{uploadedFiles[file].map((file: any, index: any) => (
156+
<div key={file.id}>
157+
{index + 1}) {file.name} - {file.size} bytes
158+
</div>
159+
))}
160+
</div>
161+
)}
162+
{file.includes('.*')
163+
? 'Drop or click to upload multiple files'
164+
: 'Drop or click to upload single file'
165+
}
166+
</Files>
167+
</div>
168+
)}
120169
<AceEditor
121170
height='200px'
122171
width='100%'

ui/src/components/elements/ApiActionResponse.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default function ApiActionResponse(props: Props) {
3232
<div className="collapse-content p-0">
3333
<AceEditor
3434
maxLines={35}
35+
readOnly={true}
3536
width='100%'
3637
mode="json"
3738
wrapEnabled={true}
@@ -54,12 +55,15 @@ export default function ApiActionResponse(props: Props) {
5455
)}
5556
{responseData && (
5657
<div className="mockup-code">
57-
<span className='pl-5 text-sm'>Response. Took: <b>{timeTaken}ms</b>, Status Code: <b>{responseStatus}</b>, Server memory: <b>{serverMemory}</b></span>
58+
<span className='pl-5 text-sm text-slate-500'>RESPONSE</span>
59+
<br />
60+
<span className='pl-5 text-sm'>Time taken: <b>{timeTaken}ms</b>, Status Code: <b>{responseStatus}</b>, Server memory: <b>{serverMemory}</b></span>
5861
<AceEditor
5962
maxLines={50}
6063
width='100%'
6164
mode="json"
6265
wrapEnabled={true}
66+
readOnly={true}
6367
value={responseData}
6468
theme="one_dark"
6569
onLoad={function (editor) { editor.renderer.setPadding(0); editor.renderer.setScrollMargin(5, 5, 5, 5); editor.renderer.setShowPrintMargin(false); }}

ui/src/components/elements/ApiActionSQL.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default function ApiActionSQL(props: Props) {
3030
<AceEditor
3131
maxLines={50}
3232
width='100%'
33+
readOnly={true}
3334
mode="sql"
3435
wrapEnabled={true}
3536
value={sqlData}

ui/src/global.css

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,3 @@ td {
9898
.response-500,.response-501,.response-502,.response-503 {
9999
@apply bg-red-400 text-red-800;
100100
}
101-
.files-dropzone {
102-
@apply bg-base-200 rounded p-5 mt-3 mb-3;
103-
}

0 commit comments

Comments
 (0)