Skip to content

(v2.6) File uploads (single and multiple) #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ npm install
npm run dev
```

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


### Developing with Laravel

#### Step 1

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

On Mac to open chrome command:
Expand Down
16 changes: 16 additions & 0 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"git-rev-sync": "^3.0.2",
"react-ace": "^10.1.0",
"react-anchor-link-smooth-scroll": "^1.0.12",
"react-files": "^3.0.0",
"react-markdown": "^8.0.5",
"react-use-localstorage": "^3.5.3",
"remark-gfm": "^3.0.1",
Expand Down
149 changes: 97 additions & 52 deletions ui/src/components/ApiAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function ApiAction(props: Props) {
const [sendingRequest, setSendingRequest] = useState(false);
const [queryParams, setQueryParams] = useState('');
const [bodyParams, setBodyParams] = useState('');
const [fileParams, setFileParams] = useState(null);
const [responseData, setResponseData] = useState("");
const [sqlQueriesCount, setSqlQueriesCount] = useState(0);
const [sqlData, setSqlData] = useState("");
Expand All @@ -37,13 +38,20 @@ export default function ApiAction(props: Props) {
const [responseHeaders, setResponseHeaders] = useState("");
const [activeTab, setActiveTab] = useState('info');

const handleFileChange = (files: any) => {
const bodyAppend = JSON.parse(bodyParams)
bodyAppend["avatar"] = files[0]
setBodyParams(JSON.stringify(bodyAppend))
const handleFileChange = (files: any, file: any) => {
const formData: any = new FormData()
if (file.includes('.*')) {
const fileParam = file.replace('.*', '')
for (let i = 0; i < files.length; i++) {
formData.append(`${fileParam}[${i}]`, files[i]);
}
} else {
formData.append(file, files[0])
}
setFileParams(formData)
}

// // update localstorage
// // update localstorage
const updateLocalStorage = () => {
const jsonAllParamsRegistry = JSON.parse(allParamsRegistry)
if (method == 'GET' || method == 'HEAD' || method == 'DELETE') {
Expand All @@ -57,7 +65,6 @@ export default function ApiAction(props: Props) {
}

const handleSendRequest = () => {
updateLocalStorage()
try {
JSON.parse(requestHeaders)
} catch (error: any) {
Expand All @@ -66,6 +73,10 @@ export default function ApiAction(props: Props) {
}
const headers = JSON.parse(requestHeaders)
headers['X-Request-LRD'] = true
if (fileParams != null) {
delete headers['Content-Type']
headers['Accept'] = 'multipart/form-data'
}

const options: any = {
credentials: "include",
Expand All @@ -76,11 +87,22 @@ export default function ApiAction(props: Props) {
if (method == 'POST' || method == 'PUT' || method == 'PATCH') {
try {
JSON.parse(bodyParams)
if (fileParams != null) {
for (const [key, value] of Object.entries(JSON.parse(bodyParams))) {
fileParams.append(key, value)
}
}

} catch (error: any) {
setError("Request body incorrect: " + error.message)
return
}
options['body'] = bodyParams

if (fileParams != null) {
options['body'] = fileParams // includes body as well
} else {
options['body'] = bodyParams // just the body
}
}

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

fetch(`${host}/${requestUri}${queryParams}`, options)
.then((response) => {
let timeTaken = performance.now() - startTime
// round to 3 decimals
timeTaken = Math.round((timeTaken + Number.EPSILON) * 1000) / 1000
setTimeTaken(timeTaken)
setResponseStatus(response.status)
setResponseHeaders(JSON.stringify(Object.fromEntries(response.headers), null, 2))
setSendingRequest(false)
return response.json();
}).then((data) => {

if (data && data._lrd && data._lrd.queries) {
const sqlQueries = data._lrd.queries.map((query: any) => {
return "Connection: "
+ query.connection_name
+ " Time taken: "
+ query.time
+ "ms: \n"
+ query.sql + "\n"
}).join("\n")
setSqlData(sqlQueries)
setSqlQueriesCount(data._lrd.queries.length)
}
if (data && data._lrd && data._lrd.logs) {
let logs = ""
for (const value of data._lrd.logs) {
logs += value.level + ": " + value.message + "\n"
.then((response) => {
let timeTaken = performance.now() - startTime
// round to 3 decimals
timeTaken = Math.round((timeTaken + Number.EPSILON) * 1000) / 1000
setTimeTaken(timeTaken)
setResponseStatus(response.status)
setResponseHeaders(JSON.stringify(Object.fromEntries(response.headers), null, 2))
setSendingRequest(false)
return response.json();
}).then((data) => {

if (data && data._lrd && data._lrd.queries) {
const sqlQueries = data._lrd.queries.map((query: any) => {
return "Connection: "
+ query.connection_name
+ " Time taken: "
+ query.time
+ "ms: \n"
+ query.sql + "\n"
}).join("\n")
setSqlData(sqlQueries)
setSqlQueriesCount(data._lrd.queries.length)
}
setLogData(logs)
}
if (data && data._lrd && data._lrd.memory) {
setServerMemory(data._lrd.memory)
}
// remove key _lrd from response
if (data && data._lrd) {
delete data._lrd
}
setResponseData(JSON.stringify(data, null, 2))
setActiveTab('response')
}).catch((error) => {
setError("Response error: " + error)
setResponseStatus(500)
setSendingRequest(false)
setActiveTab('response')
})
if (data && data._lrd && data._lrd.logs) {
let logs = ""
for (const value of data._lrd.logs) {
logs += value.level + ": " + value.message + "\n"
}
setLogData(logs)
}
if (data && data._lrd && data._lrd.memory) {
setServerMemory(data._lrd.memory)
}
// remove key _lrd from response
if (data && data._lrd) {
delete data._lrd
}
setResponseData(JSON.stringify(data, null, 2))
setActiveTab('response')
updateLocalStorage()
}).catch((error) => {
setError("Response error: " + error)
setResponseStatus(500)
setSendingRequest(false)
setActiveTab('response')
updateLocalStorage()
})

}

Expand Down Expand Up @@ -176,7 +200,27 @@ export default function ApiAction(props: Props) {
return
}
const body: any = {}
for (const [key] of Object.entries(lrdDocsItem.rules)) {
for (const [key, rule] of Object.entries(lrdDocsItem.rules)) {
if (rule.length == 0) {
continue
}
const theRule = rule[0].split("|")
if (theRule.includes('file') || theRule.includes('image')) {
continue
}
if (key.includes(".*")) {
body[key] = []
continue
}
if (key.includes(".")) {
const keys = key.split(".")
if (keys.length == 2) {
body[keys[0]] = {}
body[keys[0]][keys[1]] = ""
}
continue
}

body[key] = ""
}
const jsonBody = JSON.stringify(body, null, 2)
Expand Down Expand Up @@ -213,6 +257,7 @@ export default function ApiAction(props: Props) {
)}
{activeTab == 'request' && (
<ApiActionRequest
lrdDocsItem={lrdDocsItem}
requestUri={requestUri}
method={method}
sendingRequest={sendingRequest}
Expand Down
31 changes: 27 additions & 4 deletions ui/src/components/ApiInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import shortid from 'shortid';
import {explode} from '../libs/strings'
import type { IAPIInfo } from '../libs/types'
import { ChevronRightIcon, LinkIcon, EnvelopeIcon } from '@heroicons/react/24/solid'
import { ChevronRightIcon, LinkIcon, EnvelopeIcon } from '@heroicons/react/24/outline'

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

const { lrdDocsItem, method } = props

const [hasFile, setHasFile] = useState(false)
useEffect(() => {
//check if lrdDocsItem has rules
const files: any = []
for (const [key, rule] of Object.entries(lrdDocsItem.rules)) {
if (rule.length == 0) {
continue
}
const theRule = rule[0].split("|")
if (theRule.includes('file') || theRule.includes('image')) {
files.push(key)
}
}
setHasFile(files.length > 0)
}, [])

const StyledRule = (theRule: any): JSX.Element => {
theRule = theRule.rule
const split = theRule.split(':')
Expand Down Expand Up @@ -77,7 +93,13 @@ export default function ApiInfo(props: Props) {
</h2>
<h3 className='pt-4'>
<span className='text-sm text-slate-500'>REQUEST SCHEMA</span>
<code className='pl-2 text-xs'>application/json</code>
<code className='pl-2 text-xs'>
{hasFile ? (
'multipart/form-data'
) : (
'application/json'
)}
</code>
</h3>
<div className='pt-4'>

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

<tr key={shortid.generate()}>
<th className='param-cell'>
¬ <code className='pl-1'>
<span className='text-blue-500 pr-1'>¬</span>
<code className='pl-1'>
{key}
{lrdDocsItem.rules[key].map((rule) => (
rule.split('|').map((theRule) => (
Expand Down
Loading