Skip to content

Commit ae4ba71

Browse files
Merge pull request #153 from rakutentech/feature/upload
(v2.6) File uploads (single and multiple)
2 parents 9d52c84 + da88ab0 commit ae4ba71

14 files changed

+276
-92
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/package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"git-rev-sync": "^3.0.2",
2222
"react-ace": "^10.1.0",
2323
"react-anchor-link-smooth-scroll": "^1.0.12",
24+
"react-files": "^3.0.0",
2425
"react-markdown": "^8.0.5",
2526
"react-use-localstorage": "^3.5.3",
2627
"remark-gfm": "^3.0.1",

ui/src/components/ApiAction.tsx

Lines changed: 97 additions & 52 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') {
@@ -57,7 +65,6 @@ export default function ApiAction(props: Props) {
5765
}
5866

5967
const handleSendRequest = () => {
60-
updateLocalStorage()
6168
try {
6269
JSON.parse(requestHeaders)
6370
} catch (error: any) {
@@ -66,6 +73,10 @@ export default function ApiAction(props: Props) {
6673
}
6774
const headers = JSON.parse(requestHeaders)
6875
headers['X-Request-LRD'] = true
76+
if (fileParams != null) {
77+
delete headers['Content-Type']
78+
headers['Accept'] = 'multipart/form-data'
79+
}
6980

7081
const options: any = {
7182
credentials: "include",
@@ -76,11 +87,22 @@ export default function ApiAction(props: Props) {
7687
if (method == 'POST' || method == 'PUT' || method == 'PATCH') {
7788
try {
7889
JSON.parse(bodyParams)
90+
if (fileParams != null) {
91+
for (const [key, value] of Object.entries(JSON.parse(bodyParams))) {
92+
fileParams.append(key, value)
93+
}
94+
}
95+
7996
} catch (error: any) {
8097
setError("Request body incorrect: " + error.message)
8198
return
8299
}
83-
options['body'] = bodyParams
100+
101+
if (fileParams != null) {
102+
options['body'] = fileParams // includes body as well
103+
} else {
104+
options['body'] = bodyParams // just the body
105+
}
84106
}
85107

86108
const startTime = performance.now();
@@ -93,51 +115,53 @@ export default function ApiAction(props: Props) {
93115
setError(null)
94116

95117
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"
118+
.then((response) => {
119+
let timeTaken = performance.now() - startTime
120+
// round to 3 decimals
121+
timeTaken = Math.round((timeTaken + Number.EPSILON) * 1000) / 1000
122+
setTimeTaken(timeTaken)
123+
setResponseStatus(response.status)
124+
setResponseHeaders(JSON.stringify(Object.fromEntries(response.headers), null, 2))
125+
setSendingRequest(false)
126+
return response.json();
127+
}).then((data) => {
128+
129+
if (data && data._lrd && data._lrd.queries) {
130+
const sqlQueries = data._lrd.queries.map((query: any) => {
131+
return "Connection: "
132+
+ query.connection_name
133+
+ " Time taken: "
134+
+ query.time
135+
+ "ms: \n"
136+
+ query.sql + "\n"
137+
}).join("\n")
138+
setSqlData(sqlQueries)
139+
setSqlQueriesCount(data._lrd.queries.length)
123140
}
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-
})
141+
if (data && data._lrd && data._lrd.logs) {
142+
let logs = ""
143+
for (const value of data._lrd.logs) {
144+
logs += value.level + ": " + value.message + "\n"
145+
}
146+
setLogData(logs)
147+
}
148+
if (data && data._lrd && data._lrd.memory) {
149+
setServerMemory(data._lrd.memory)
150+
}
151+
// remove key _lrd from response
152+
if (data && data._lrd) {
153+
delete data._lrd
154+
}
155+
setResponseData(JSON.stringify(data, null, 2))
156+
setActiveTab('response')
157+
updateLocalStorage()
158+
}).catch((error) => {
159+
setError("Response error: " + error)
160+
setResponseStatus(500)
161+
setSendingRequest(false)
162+
setActiveTab('response')
163+
updateLocalStorage()
164+
})
141165

142166
}
143167

@@ -176,7 +200,27 @@ export default function ApiAction(props: Props) {
176200
return
177201
}
178202
const body: any = {}
179-
for (const [key] of Object.entries(lrdDocsItem.rules)) {
203+
for (const [key, rule] of Object.entries(lrdDocsItem.rules)) {
204+
if (rule.length == 0) {
205+
continue
206+
}
207+
const theRule = rule[0].split("|")
208+
if (theRule.includes('file') || theRule.includes('image')) {
209+
continue
210+
}
211+
if (key.includes(".*")) {
212+
body[key] = []
213+
continue
214+
}
215+
if (key.includes(".")) {
216+
const keys = key.split(".")
217+
if (keys.length == 2) {
218+
body[keys[0]] = {}
219+
body[keys[0]][keys[1]] = ""
220+
}
221+
continue
222+
}
223+
180224
body[key] = ""
181225
}
182226
const jsonBody = JSON.stringify(body, null, 2)
@@ -213,6 +257,7 @@ export default function ApiAction(props: Props) {
213257
)}
214258
{activeTab == 'request' && (
215259
<ApiActionRequest
260+
lrdDocsItem={lrdDocsItem}
216261
requestUri={requestUri}
217262
method={method}
218263
sendingRequest={sendingRequest}

ui/src/components/ApiInfo.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React from 'react';
1+
import React, { useState, useEffect } from 'react';
22
import shortid from 'shortid';
33
import {explode} from '../libs/strings'
44
import type { IAPIInfo } from '../libs/types'
5-
import { ChevronRightIcon, LinkIcon, EnvelopeIcon } from '@heroicons/react/24/solid'
5+
import { ChevronRightIcon, LinkIcon, EnvelopeIcon } from '@heroicons/react/24/outline'
66

77
interface Props {
88
lrdDocsItem: IAPIInfo,
@@ -12,6 +12,22 @@ export default function ApiInfo(props: Props) {
1212

1313
const { lrdDocsItem, method } = props
1414

15+
const [hasFile, setHasFile] = useState(false)
16+
useEffect(() => {
17+
//check if lrdDocsItem has rules
18+
const files: any = []
19+
for (const [key, rule] of Object.entries(lrdDocsItem.rules)) {
20+
if (rule.length == 0) {
21+
continue
22+
}
23+
const theRule = rule[0].split("|")
24+
if (theRule.includes('file') || theRule.includes('image')) {
25+
files.push(key)
26+
}
27+
}
28+
setHasFile(files.length > 0)
29+
}, [])
30+
1531
const StyledRule = (theRule: any): JSX.Element => {
1632
theRule = theRule.rule
1733
const split = theRule.split(':')
@@ -77,7 +93,13 @@ export default function ApiInfo(props: Props) {
7793
</h2>
7894
<h3 className='pt-4'>
7995
<span className='text-sm text-slate-500'>REQUEST SCHEMA</span>
80-
<code className='pl-2 text-xs'>application/json</code>
96+
<code className='pl-2 text-xs'>
97+
{hasFile ? (
98+
'multipart/form-data'
99+
) : (
100+
'application/json'
101+
)}
102+
</code>
81103
</h3>
82104
<div className='pt-4'>
83105

@@ -87,7 +109,8 @@ export default function ApiInfo(props: Props) {
87109

88110
<tr key={shortid.generate()}>
89111
<th className='param-cell'>
90-
¬ <code className='pl-1'>
112+
<span className='text-blue-500 pr-1'>¬</span>
113+
<code className='pl-1'>
91114
{key}
92115
{lrdDocsItem.rules[key].map((rule) => (
93116
rule.split('|').map((theRule) => (

0 commit comments

Comments
 (0)