index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. <template>
  2. <div class="page">
  3. <pw-section class="blue" label="Request" ref="request">
  4. <ul>
  5. <li>
  6. <label for="method">Method</label>
  7. <select id="method" v-model="method">
  8. <option>GET</option>
  9. <option>HEAD</option>
  10. <option>POST</option>
  11. <option>PUT</option>
  12. <option>DELETE</option>
  13. <option>OPTIONS</option>
  14. <option>PATCH</option>
  15. </select>
  16. </li>
  17. <li>
  18. <label for="url">URL</label>
  19. <input id="url" type="url" :class="{ error: !isValidURL }" v-model="url" @keyup.enter="isValidURL ? sendRequest() : null">
  20. </li>
  21. <li>
  22. <label for="path">Path</label>
  23. <input id="path" v-model="path" @keyup.enter="isValidURL ? sendRequest() : null">
  24. </li>
  25. <li>
  26. <label for="action" class="hide-on-small-screen">&nbsp;</label>
  27. <button id="action" class="show" name="action" @click="sendRequest" :disabled="!isValidURL" ref="sendButton">Send <span id="hidden-message">Again</span></button>
  28. </li>
  29. </ul>
  30. </pw-section>
  31. <pw-section class="blue-dark" label="Request Body" v-if="method === 'POST' || method === 'PUT' || method === 'PATCH'">
  32. <ul>
  33. <li>
  34. <label>Content Type</label>
  35. <select v-model="contentType">
  36. <option>application/json</option>
  37. <option>www-form/urlencoded</option>
  38. </select>
  39. <span>
  40. <input v-model="rawInput" style="cursor: pointer;" type="checkbox" id="rawInput">
  41. <label for="rawInput" style="cursor: pointer;">Raw Input</label>
  42. </span>
  43. </li>
  44. </ul>
  45. <div v-if="!rawInput">
  46. <ol v-for="(param, index) in bodyParams">
  47. <li>
  48. <label :for="'bparam'+index">Key {{index + 1}}</label>
  49. <input :name="'bparam'+index" v-model="param.key">
  50. </li>
  51. <li>
  52. <label :for="'bvalue'+index">Value {{index + 1}}</label>
  53. <input :name="'bvalue'+index" v-model="param.value">
  54. </li>
  55. <li>
  56. <label for="request" class="hide-on-small-screen">&nbsp;</label>
  57. <button name="request" @click="removeRequestBodyParam(index)">Remove</button>
  58. </li>
  59. </ol>
  60. <ul>
  61. <li>
  62. <label for="addrequest">Action</label>
  63. <button name="addrequest" @click="addRequestBodyParam">Add</button>
  64. </li>
  65. </ul>
  66. <ul>
  67. <li>
  68. <label for="request">Parameter List</label>
  69. <textarea name="request" rows="1" v-textarea-auto-height="rawRequestBody" readonly>{{rawRequestBody || '(add at least one parameter)'}}</textarea>
  70. </li>
  71. </ul>
  72. </div>
  73. <div v-else>
  74. <textarea v-model="rawParams" v-textarea-auto-height="rawParams" style="font-family: monospace;" rows="16" @keydown="formatRawParams"></textarea>
  75. </div>
  76. </pw-section>
  77. <pw-section class="green" label="Authentication" collapsed>
  78. <ul>
  79. <li>
  80. <label for="auth">Authentication Type</label>
  81. <select v-model="auth">
  82. <option>None</option>
  83. <option>Basic</option>
  84. <option>Bearer Token</option>
  85. </select>
  86. </li>
  87. </ul>
  88. <ul v-if="auth === 'Basic'">
  89. <li>
  90. <label for="http_basic_user">User</label>
  91. <input v-model="httpUser">
  92. </li>
  93. <li>
  94. <label for="http_basic_passwd">Password</label>
  95. <input v-model="httpPassword" type="password">
  96. </li>
  97. </ul>
  98. <ul v-if="auth === 'Bearer Token'">
  99. <li>
  100. <label for="bearer_token">Token</label>
  101. <input v-model="bearerToken">
  102. </li>
  103. </ul>
  104. </pw-section>
  105. <pw-section class="orange" label="Headers" collapsed>
  106. <ol v-for="(header, index) in headers">
  107. <li>
  108. <label :for="'header'+index">Key {{index + 1}}</label>
  109. <input :name="'header'+index" v-model="header.key">
  110. </li>
  111. <li>
  112. <label :for="'value'+index">Value {{index + 1}}</label>
  113. <input :name="'value'+index" v-model="header.value">
  114. </li>
  115. <li>
  116. <label for="header" class="hide-on-small-screen">&nbsp;</label>
  117. <button name="header" @click="removeRequestHeader(index)">Remove</button>
  118. </li>
  119. </ol>
  120. <ul>
  121. <li>
  122. <label for="add">Action</label>
  123. <button name="add" @click="addRequestHeader">Add</button>
  124. </li>
  125. </ul>
  126. <ul>
  127. <li>
  128. <label for="request">Header List</label>
  129. <textarea v-textarea-auto-height="headerString" name="request" rows="1" readonly ref="requestTextarea">{{headerString || '(add at least one header)'}}</textarea>
  130. </li>
  131. </ul>
  132. </pw-section>
  133. <pw-section class="cyan" label="Parameters" collapsed>
  134. <ol v-for="(param, index) in params">
  135. <li>
  136. <label :for="'param'+index">Key {{index + 1}}</label>
  137. <input :name="'param'+index" v-model="param.key">
  138. </li>
  139. <li>
  140. <label :for="'value'+index">Value {{index + 1}}</label>
  141. <input :name="'value'+index" v-model="param.value">
  142. </li>
  143. <li>
  144. <label for="param" class="hide-on-small-screen">&nbsp;</label>
  145. <button name="param" @click="removeRequestParam(index)">Remove</button>
  146. </li>
  147. </ol>
  148. <ul>
  149. <li>
  150. <label for="add">Action</label>
  151. <button name="add" @click="addRequestParam">Add</button>
  152. </li>
  153. </ul>
  154. <ul>
  155. <li>
  156. <label for="request">Parameter List</label>
  157. <textarea name="request" v-textarea-auto-height="queryString" rows="1" readonly>{{queryString || '(add at least one parameter)'}}</textarea>
  158. </li>
  159. </ul>
  160. </pw-section>
  161. <pw-section class="purple" label="Response" id="response" ref="response">
  162. <ul>
  163. <li>
  164. <label for="status">status</label>
  165. <input name="status" type="text" readonly :value="response.status || '(waiting to send request)'" :class="statusCategory ? statusCategory.className : ''">
  166. </li>
  167. </ul>
  168. <ul v-for="(value, key) in response.headers">
  169. <li>
  170. <label for="value">{{key}}</label>
  171. <input name="value" :value="value" readonly>
  172. </li>
  173. </ul>
  174. <ul>
  175. <li>
  176. <div class="flex-wrap">
  177. <label for="body">response</label>
  178. <button v-if="response.body" name="action" @click="copyResponse">Copy Response</button>
  179. </div>
  180. <div id="response-details-wrapper">
  181. <textarea name="body" rows="16" id="response-details" readonly>{{response.body || '(waiting to send request)'}}</textarea>
  182. <iframe src="about:blank" class="covers-response" ref="previewFrame" :class="{hidden: !previewEnabled}"></iframe>
  183. </div>
  184. <div v-if="response.body && responseType === 'text/html'" class="align-right">
  185. <button @click.prevent="togglePreview">{{ previewEnabled ? 'Hide Preview' : 'Preview HTML' }}</button>
  186. </div>
  187. </li>
  188. </ul>
  189. </pw-section>
  190. <history @useHistory="handleUseHistory" ref="historyComponent" />
  191. </div>
  192. </template>
  193. <script>
  194. import history from "../components/history";
  195. import section from "../components/section";
  196. import textareaAutoHeight from "../directives/textareaAutoHeight";
  197. const statusCategories = [{
  198. name: 'informational',
  199. statusCodeRegex: new RegExp(/[1][0-9]+/),
  200. className: 'info-response'
  201. },
  202. {
  203. name: 'successful',
  204. statusCodeRegex: new RegExp(/[2][0-9]+/),
  205. className: 'success-response'
  206. },
  207. {
  208. name: 'redirection',
  209. statusCodeRegex: new RegExp(/[3][0-9]+/),
  210. className: 'redir-response'
  211. },
  212. {
  213. name: 'client error',
  214. statusCodeRegex: new RegExp(/[4][0-9]+/),
  215. className: 'cl-error-response'
  216. },
  217. {
  218. name: 'server error',
  219. statusCodeRegex: new RegExp(/[5][0-9]+/),
  220. className: 'sv-error-response'
  221. },
  222. {
  223. // this object is a catch-all for when no other objects match and should always be last
  224. name: 'unknown',
  225. statusCodeRegex: new RegExp(/.*/),
  226. className: 'missing-data-response'
  227. }
  228. ];
  229. const parseHeaders = xhr => {
  230. const headers = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
  231. const headerMap = {};
  232. headers.forEach(line => {
  233. const parts = line.split(': ');
  234. const header = parts.shift().toLowerCase();
  235. const value = parts.join(': ');
  236. headerMap[header] = value
  237. });
  238. return headerMap
  239. };
  240. export const findStatusGroup = responseStatus => statusCategories.find(status => status.statusCodeRegex.test(responseStatus));
  241. export default {
  242. middleware: 'parsedefaulturl', // calls middleware before loading the page
  243. directives: {
  244. textareaAutoHeight
  245. },
  246. components: {
  247. 'pw-section': section,
  248. history
  249. },
  250. data() {
  251. return {
  252. method: 'GET',
  253. url: 'https://reqres.in',
  254. auth: 'None',
  255. path: '/api/users',
  256. httpUser: '',
  257. httpPassword: '',
  258. bearerToken: '',
  259. headers: [],
  260. params: [],
  261. bodyParams: [],
  262. rawParams: '',
  263. rawInput: false,
  264. contentType: 'application/json',
  265. response: {
  266. status: '',
  267. headers: '',
  268. body: ''
  269. },
  270. previewEnabled: false
  271. }
  272. },
  273. computed: {
  274. statusCategory() {
  275. return findStatusGroup(this.response.status);
  276. },
  277. isValidURL() {
  278. const protocol = '^(https?:\\/\\/)?';
  279. const validIP = new RegExp(protocol + "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$");
  280. const validHostname = new RegExp(protocol + "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$");
  281. return validIP.test(this.url) || validHostname.test(this.url);
  282. },
  283. hasRequestBody() {
  284. return ['POST', 'PUT', 'PATCH'].includes(this.method);
  285. },
  286. rawRequestBody() {
  287. const {
  288. bodyParams
  289. } = this
  290. if (this.contentType === 'application/json') {
  291. try {
  292. const obj = JSON.parse(`{${bodyParams.filter(({ key }) => !!key).map(({ key, value }) => `
  293. "${key}": "${value}"
  294. `).join()}}`)
  295. return JSON.stringify(obj)
  296. } catch (ex) {
  297. return 'invalid'
  298. }
  299. } else {
  300. return bodyParams
  301. .filter(({
  302. key
  303. }) => !!key)
  304. .map(({
  305. key,
  306. value
  307. }) => `${key}=${encodeURIComponent(value)}`).join('&')
  308. }
  309. },
  310. headerString() {
  311. const result = this.headers
  312. .filter(({
  313. key
  314. }) => !!key)
  315. .map(({
  316. key,
  317. value
  318. }) => `${key}: ${value}`).join(',\n')
  319. return result == '' ? '' : `${result}`
  320. },
  321. queryString() {
  322. const result = this.params
  323. .filter(({
  324. key
  325. }) => !!key)
  326. .map(({
  327. key,
  328. value
  329. }) => `${key}=${encodeURIComponent(value)}`).join('&')
  330. return result === '' ? '' : `?${result}`
  331. },
  332. responseType() {
  333. return (this.response.headers['content-type'] || '').split(';')[0].toLowerCase();
  334. }
  335. },
  336. methods: {
  337. handleUseHistory({
  338. method,
  339. url,
  340. path
  341. }) {
  342. this.method = method;
  343. this.url = url;
  344. this.path = path;
  345. this.$refs.request.$el.scrollIntoView({
  346. behavior: 'smooth'
  347. });
  348. },
  349. async sendRequest() {
  350. if (!this.isValidURL) {
  351. alert('Please check the formatting of the URL.');
  352. return;
  353. }
  354. // Start showing the loading bar as soon as possible.
  355. // The nuxt axios module will hide it when the request is made.
  356. this.$nuxt.$loading.start();
  357. if (this.$refs.response.$el.classList.contains('hidden')) {
  358. this.$refs.response.$el.classList.toggle('hidden')
  359. }
  360. this.$refs.response.$el.scrollIntoView({
  361. behavior: 'smooth'
  362. });
  363. this.previewEnabled = false;
  364. this.response.status = 'Fetching...';
  365. this.response.body = 'Loading...';
  366. const auth = this.auth === 'Basic' ? {
  367. username: this.httpUser,
  368. password: this.httpPassword
  369. } : null;
  370. let headers = {};
  371. // If the request has a request body, we want to ensure Content-Length and
  372. // Content-Type are sent.
  373. let requestBody;
  374. if (this.hasRequestBody) {
  375. requestBody = this.rawInput ? this.rawParams : this.rawRequestBody;
  376. Object.assign(headers, {
  377. 'Content-Length': requestBody.length,
  378. 'Content-Type': `${this.contentType}; charset=utf-8`
  379. });
  380. }
  381. // If the request uses a token for auth, we want to make sure it's sent here.
  382. if (this.auth === 'Bearer Token') headers['Authorization'] = `Bearer ${this.bearerToken}`;
  383. headers = Object.assign(
  384. // Clone the app headers object first, we don't want to
  385. // mutate it with the request headers added by default.
  386. Object.assign({}, this.headers),
  387. // We make our temporary headers object the source so
  388. // that you can override the added headers if you
  389. // specify them.
  390. headers
  391. );
  392. try {
  393. const payload = await this.$axios({
  394. method: this.method,
  395. url: this.url + this.path + this.queryString,
  396. auth,
  397. headers,
  398. data: requestBody
  399. });
  400. (() => {
  401. const status = this.response.status = payload.status;
  402. const headers = this.response.headers = payload.headers;
  403. // We don't need to bother parsing JSON, axios already handles it for us!
  404. const body = this.response.body = payload.data;
  405. const date = new Date().toLocaleDateString();
  406. const time = new Date().toLocaleTimeString();
  407. // Addition of an entry to the history component.
  408. const entry = {
  409. status,
  410. date,
  411. time,
  412. method: this.method,
  413. url: this.url,
  414. path: this.path
  415. };
  416. this.$refs.historyComponent.addEntry(entry);
  417. })();
  418. } catch (error) {
  419. if (error.response) {
  420. this.response.headers = error.response.headers;
  421. this.response.status = error.response.status;
  422. this.response.body = error.response.data;
  423. // Addition of an entry to the history component.
  424. const entry = {
  425. status: this.response.status,
  426. date: new Date().toLocaleDateString(),
  427. time: new Date().toLocaleTimeString(),
  428. method: this.method,
  429. url: this.url,
  430. path: this.path
  431. };
  432. this.$refs.historyComponent.addEntry(entry);
  433. return;
  434. }
  435. this.response.status = error.message;
  436. this.response.body = "See JavaScript console (F12) for details.";
  437. }
  438. },
  439. addRequestHeader() {
  440. this.headers.push({
  441. key: '',
  442. value: ''
  443. });
  444. return false
  445. },
  446. removeRequestHeader(index) {
  447. this.headers.splice(index, 1)
  448. },
  449. addRequestParam() {
  450. this.params.push({
  451. key: '',
  452. value: ''
  453. })
  454. return false
  455. },
  456. removeRequestParam(index) {
  457. this.params.splice(index, 1)
  458. },
  459. addRequestBodyParam() {
  460. this.bodyParams.push({
  461. key: '',
  462. value: ''
  463. })
  464. return false
  465. },
  466. removeRequestBodyParam(index) {
  467. this.bodyParams.splice(index, 1)
  468. },
  469. formatRawParams(event) {
  470. if ((event.which !== 13 && event.which !== 9)) {
  471. return;
  472. }
  473. const textBody = event.target.value;
  474. const textBeforeCursor = textBody.substring(0, event.target.selectionStart);
  475. const textAfterCursor = textBody.substring(event.target.selectionEnd);
  476. if (event.which === 13) {
  477. event.preventDefault();
  478. const oldSelectionStart = event.target.selectionStart;
  479. const lastLine = textBeforeCursor.split('\n').slice(-1)[0];
  480. const rightPadding = lastLine.match(/([\s\t]*).*/)[1] || "";
  481. event.target.value = textBeforeCursor + '\n' + rightPadding + textAfterCursor;
  482. setTimeout(() => event.target.selectionStart = event.target.selectionEnd = oldSelectionStart + rightPadding.length + 1, 1);
  483. } else if (event.which === 9) {
  484. event.preventDefault();
  485. const oldSelectionStart = event.target.selectionStart;
  486. event.target.value = textBeforeCursor + '\xa0\xa0' + textAfterCursor;
  487. event.target.selectionStart = event.target.selectionEnd = oldSelectionStart + 2;
  488. return false;
  489. }
  490. },
  491. copyResponse() {
  492. var copyText = document.getElementById("response-details");
  493. copyText.select();
  494. document.execCommand("copy");
  495. },
  496. togglePreview() {
  497. this.previewEnabled = !this.previewEnabled;
  498. if (this.previewEnabled) {
  499. // If you want to add 'preview' support for other response types,
  500. // just add them here.
  501. if (this.responseType === "text/html") {
  502. // If the preview already has that URL loaded, let's not bother re-loading it all.
  503. if (this.$refs.previewFrame.getAttribute('data-previewing-url') === this.url)
  504. return;
  505. // Use DOMParser to parse document HTML.
  506. const previewDocument = new DOMParser().parseFromString(this.response.body, this.responseType);
  507. // Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
  508. previewDocument.head.innerHTML = `<base href="${this.url}">` + previewDocument.head.innerHTML;
  509. // Finally, set the iframe source to the resulting HTML.
  510. this.$refs.previewFrame.srcdoc = previewDocument.documentElement.outerHTML;
  511. this.$refs.previewFrame.setAttribute('data-previewing-url', this.url);
  512. }
  513. }
  514. },
  515. setRouteQueries(queries) {
  516. for (const key in queries) {
  517. if (this[key]) this[key] = queries[key];
  518. }
  519. },
  520. observeRequestButton() {
  521. const requestElement = this.$refs.request.$el;
  522. const sendButtonElement = this.$refs.sendButton;
  523. const observer = new IntersectionObserver((entries, observer) => {
  524. entries.forEach(entry => {
  525. sendButtonElement.classList.toggle('show');
  526. });
  527. }, { threshold: 1 });
  528. observer.observe(requestElement);
  529. }
  530. },
  531. created() {
  532. if (Object.keys(this.$route.query).length) this.setRouteQueries(this.$route.query);
  533. },
  534. mounted() {
  535. this.observeRequestButton();
  536. }
  537. }
  538. </script>