Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -35,10 +35,10 @@ notifications:
|
||||
|
||||
deploy:
|
||||
provider: pages
|
||||
skip_cleanup: true
|
||||
skip-cleanup: true
|
||||
# Refer to: https://docs.travis-ci.com/user/deployment/pages/#Setting-the-GitHub-token
|
||||
github_token: $GITHUB_ACCESS_TOKEN
|
||||
github-token: $GITHUB_ACCESS_TOKEN
|
||||
target-branch: gh-pages
|
||||
local_dir: dist
|
||||
local-dir: dist
|
||||
on:
|
||||
branch: master
|
||||
branch: master
|
||||
|
||||
@@ -13,7 +13,7 @@ When I wrote this, only God and I understood what I was doing. Now, only God kno
|
||||
|
||||
---
|
||||
|
||||
[](https://travis-ci.org/liyasthomas/postwoman) [](https://github.com/liyasthomas/postwoman/releases/latest) [](https://github.com/liyasthomas/postwoman/archive/master.zip) [](https://github.com/liyasthomas/postwoman/blob/master/LICENSE) [](https://github.com/liyasthomas/postwoman/issues) [](https://liyas-thomas.firebaseapp.com) [](https://www.paypal.me/liyascthomas) [](https://www.buymeacoffee.com/liyasthomas)
|
||||
[](https://travis-ci.com/liyasthomas/postwoman) [](https://github.com/liyasthomas/postwoman/releases/latest) [](https://github.com/liyasthomas/postwoman/archive/master.zip) [](https://github.com/liyasthomas/postwoman/blob/master/LICENSE) [](https://github.com/liyasthomas/postwoman/issues) [](https://liyas-thomas.firebaseapp.com) [](https://www.paypal.me/liyascthomas) [](https://www.buymeacoffee.com/liyasthomas)
|
||||
|
||||
# <img src="static/icon.png" alt="postwoman" width="32"> Postwoman
|
||||
|
||||
@@ -88,7 +88,7 @@ Please read [CONTRIBUTING](CONTRIBUTING.md) for details on our [CODE OF CONDUCT]
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
We use [Travis CI](https://travis-ci.com) for continuous integration. Check out our [Travis CI Status](https://travis-ci.org/liyasthomas/postwoman).
|
||||
We use [Travis CI](https://travis-ci.com) for continuous integration. Check out our [Travis CI Status](https://travis-ci.com/liyasthomas/postwoman).
|
||||
|
||||
---
|
||||
|
||||
@@ -113,8 +113,13 @@ See the [CHANGELOG](CHANGELOG.md) file for details.
|
||||
* [Liyas Thomas](https://github.com/liyasthomas)
|
||||
|
||||
### Contributors
|
||||
* [NBTX](https://github.com/NBTX)
|
||||
* [Andrew Bastin](https://github.com/AndrewBastin)
|
||||
* [Nick Palenchar](https://github.com/nickpalenchar)
|
||||
* [Abraham Williams](https://github.com/abraham)
|
||||
* [Nicholas La Roux](https://github.com/larouxn)
|
||||
* [RifqiAlAbqary](https://github.com/reefqi037)
|
||||
* [izerozlu](https://github.com/izerozlu)
|
||||
|
||||
### Thanks
|
||||
* [Dribbble](https://dribbble.com)
|
||||
|
||||
@@ -234,6 +234,16 @@ ol li {
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.flex-wrap{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.btn-copy{
|
||||
padding: 6px 14px;
|
||||
font-size: 11px;
|
||||
margin-right: 15px;
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: $responsiveWidth) {
|
||||
ul,
|
||||
@@ -252,21 +262,34 @@ ol li {
|
||||
}
|
||||
|
||||
.info-response {
|
||||
background-color: #ffeb3b;
|
||||
background-color: #FFEB3B;
|
||||
}
|
||||
|
||||
.success-response {
|
||||
background-color: #66BB6A;
|
||||
background-color: #4BB543;
|
||||
}
|
||||
|
||||
.redir-response {
|
||||
background-color: #ff5722;
|
||||
background-color: #FF5722;
|
||||
}
|
||||
|
||||
.cl-error-response {
|
||||
background-color: #ef5350;
|
||||
background-color: #A63232;
|
||||
}
|
||||
|
||||
.sv-error-response {
|
||||
background-color: #b71c1c;
|
||||
}
|
||||
background-color: #B71C1C;
|
||||
}
|
||||
|
||||
fieldset#history {
|
||||
.method-list-item {
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
right: 20px;
|
||||
font-family: monospace, monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fieldset :class="{ 'no-colored-frames': noFrameColors }">
|
||||
<fieldset :id="label.toLowerCase()" :class="{ 'no-colored-frames': noFrameColors }">
|
||||
<legend @click.prevent="collapse">{{ label }} ↕</legend>
|
||||
<div class="collapsible" :class="{ hidden: collapsed }">
|
||||
<slot />
|
||||
@@ -44,4 +44,4 @@
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<nuxt-link to="/">
|
||||
<h1 class="logo"><logo alt="" style="height: 24px; margin-right: 16px"/>Postwoman</h1>
|
||||
</nuxt-link>
|
||||
<h3>Lightweight API request builder</h3>
|
||||
<h3>API request builder</h3>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// TODO: Use these when rendering the pages (rather than just for head/meta tags...)
|
||||
export const meta = {
|
||||
name: "Postwoman",
|
||||
shortDescription: "Lightweight API request builder",
|
||||
shortDescription: "API request builder",
|
||||
description: "The Postwoman API request builder helps you create your requests faster, saving you precious time on your development."
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ export default {
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no, minimal-ui' },
|
||||
{ hid: 'description', name: 'description', content: meta.description || '' },
|
||||
{ name: 'keywords', content: 'postwoman, api, request, testing, tool, rest, websocket'},
|
||||
|
||||
{ name: 'X-UA-Compatible', content: "IE=edge, chrome=1" },
|
||||
{ itemprop: "name", content: `${meta.name} \u2022 ${meta.shortDescription}` },
|
||||
|
||||
205
pages/index.vue
205
pages/index.vue
@@ -36,34 +36,42 @@
|
||||
<option>application/json</option>
|
||||
<option>www-form/urlencoded</option>
|
||||
</select>
|
||||
<span>
|
||||
<input v-model="rawInput" style="cursor: pointer;" type="checkbox" id="rawInput">
|
||||
<label for="rawInput" style="cursor: pointer;">Raw Input</label>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ol v-for="(param, index) in bodyParams">
|
||||
<li>
|
||||
<label :for="'bparam'+index">Key {{index + 1}}</label>
|
||||
<input :name="'bparam'+index" v-model="param.key">
|
||||
</li>
|
||||
<li>
|
||||
<label :for="'bvalue'+index">Value {{index + 1}}</label>
|
||||
<input :name="'bvalue'+index" v-model="param.value">
|
||||
</li>
|
||||
<li>
|
||||
<label for="request"> </label>
|
||||
<button name="request" @click="removeRequestBodyParam(index)">Remove</button>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="addrequest">Action</label>
|
||||
<button name="addrequest" @click="addRequestBodyParam">Add</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="request">Parameter List</label>
|
||||
<textarea name="request" rows="1" readonly>{{rawRequestBody || '(add at least one parameter)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="!rawInput">
|
||||
<ol v-for="(param, index) in bodyParams">
|
||||
<li>
|
||||
<label :for="'bparam'+index">Key {{index + 1}}</label>
|
||||
<input :name="'bparam'+index" v-model="param.key">
|
||||
</li>
|
||||
<li>
|
||||
<label :for="'bvalue'+index">Value {{index + 1}}</label>
|
||||
<input :name="'bvalue'+index" v-model="param.value">
|
||||
</li>
|
||||
<li>
|
||||
<label for="request"> </label>
|
||||
<button name="request" @click="removeRequestBodyParam(index)">Remove</button>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="addrequest">Action</label>
|
||||
<button name="addrequest" @click="addRequestBodyParam">Add</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="request">Parameter List</label>
|
||||
<textarea name="request" rows="1" readonly>{{rawRequestBody || '(add at least one parameter)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</div><div v-else>
|
||||
<textarea v-model="rawParams" style="font-family: monospace;" rows="16" @keydown="formatRawParams"></textarea>
|
||||
</div>
|
||||
</pw-section>
|
||||
|
||||
<pw-section class="green" label="Authentication" collapsed>
|
||||
@@ -138,9 +146,12 @@
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<li>
|
||||
<div class="flex-wrap">
|
||||
<label for="body">response</label>
|
||||
<textarea name="body" rows="10" readonly>{{response.body || '(waiting to send request)'}}</textarea>
|
||||
<button v-if="response.body" name="action" class="btn-copy" @click="copyResponse">Copy Response</button>
|
||||
</div>
|
||||
<textarea name="body" rows="10" id="response-details" readonly>{{response.body || '(waiting to send request)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
@@ -156,17 +167,19 @@
|
||||
<label for="time">Time</label>
|
||||
<input name="time" type="text" readonly :value="entry.time">
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">Method</label>
|
||||
<input name="name" type="text" readonly :value="entry.method">
|
||||
<li class="method-list-item">
|
||||
<label for="method">Method</label>
|
||||
<input name="method" type="text" readonly
|
||||
:value="entry.method" :class="findEntryStatus(entry).className" :style="{'--status-code': entry.status}">
|
||||
<span class="entry-status-code">{{entry.status}}</span>
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">URL</label>
|
||||
<input name="name" type="text" readonly :value="entry.url">
|
||||
<label for="url">URL</label>
|
||||
<input name="url" type="text" readonly :value="entry.url">
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">Path</label>
|
||||
<input name="name" type="text" readonly :value="entry.path">
|
||||
<label for="path">Path</label>
|
||||
<input name="path" type="text" readonly :value="entry.path">
|
||||
</li>
|
||||
<li>
|
||||
<label for="delete"> </label>
|
||||
@@ -183,22 +196,33 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const parseHeaders = xhr => {
|
||||
const headers = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
|
||||
const headerMap = {};
|
||||
headers.forEach(line => {
|
||||
const parts = line.split(': ');
|
||||
const header = parts.shift().toLowerCase();
|
||||
const value = parts.join(': ');
|
||||
headerMap[header] = value
|
||||
});
|
||||
return headerMap
|
||||
};
|
||||
const statusCategories = [
|
||||
{name: 'informational', statusCodeRegex: new RegExp(/[1][0-9]+/), className: 'info-response'},
|
||||
{name: 'successful', statusCodeRegex: new RegExp(/[2][0-9]+/), className: 'success-response'},
|
||||
{name: 'redirection', statusCodeRegex: new RegExp(/[3][0-9]+/), className: 'redir-response'},
|
||||
{name: 'client error', statusCodeRegex: new RegExp(/[4][0-9]+/), className: 'cl-error-response'},
|
||||
{name: 'server error', statusCodeRegex: new RegExp(/[5][0-9]+/), className: 'sv-error-response'},
|
||||
];
|
||||
|
||||
import section from "../components/section";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
const parseHeaders = xhr => {
|
||||
const headers = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
|
||||
const headerMap = {};
|
||||
headers.forEach(line => {
|
||||
const parts = line.split(': ');
|
||||
const header = parts.shift().toLowerCase();
|
||||
const value = parts.join(': ');
|
||||
headerMap[header] = value
|
||||
});
|
||||
return headerMap
|
||||
};
|
||||
|
||||
const findStatusGroup = responseStatus => statusCategories.find(status => status.statusCodeRegex.test(responseStatus));
|
||||
|
||||
import section from "../components/section";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'pw-section': section
|
||||
},
|
||||
|
||||
@@ -213,6 +237,8 @@
|
||||
bearerToken: '',
|
||||
params: [],
|
||||
bodyParams: [],
|
||||
rawParams: '',
|
||||
rawInput: false,
|
||||
contentType: 'application/json',
|
||||
response: {
|
||||
status: '',
|
||||
@@ -224,13 +250,7 @@
|
||||
},
|
||||
computed: {
|
||||
statusCategory(){
|
||||
return [
|
||||
{name: 'informational', statusCodeRegex: new RegExp(/[1][0-9]+/), className: 'info-response'},
|
||||
{name: 'successful', statusCodeRegex: new RegExp(/[2][0-9]+/), className: 'success-response'},
|
||||
{name: 'redirection', statusCodeRegex: new RegExp(/[3][0-9]+/), className: 'redir-response'},
|
||||
{name: 'client error', statusCodeRegex: new RegExp(/[4][0-9]+/), className: 'cl-error-response'},
|
||||
{name: 'server error', statusCodeRegex: new RegExp(/[5][0-9]+/), className: 'sv-error-response'},
|
||||
].find(status => status.statusCodeRegex.test(this.response.status));
|
||||
return findStatusGroup(this.response.status);
|
||||
},
|
||||
noHistoryToClear() {
|
||||
return this.history.length === 0;
|
||||
@@ -280,6 +300,9 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findEntryStatus(entry){
|
||||
return findStatusGroup(entry.status);
|
||||
},
|
||||
deleteHistory(entry) {
|
||||
this.history.splice(this.history.indexOf(entry), 1)
|
||||
window.localStorage.setItem('history', JSON.stringify(this.history))
|
||||
@@ -301,17 +324,6 @@
|
||||
})
|
||||
},
|
||||
sendRequest() {
|
||||
if (!this.isValidURL) {
|
||||
alert('Please check the formatting of the URL');
|
||||
return
|
||||
}
|
||||
const n = new Date().toLocaleTimeString()
|
||||
this.history = [{
|
||||
time: n,
|
||||
method: this.method,
|
||||
url: this.url,
|
||||
path: this.path
|
||||
}, ...this.history]
|
||||
window.localStorage.setItem('history', JSON.stringify(this.history))
|
||||
if (this.$refs.response.$el.classList.contains('hidden')) {
|
||||
this.$refs.response.$el.classList.toggle('hidden')
|
||||
@@ -329,7 +341,7 @@
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + this.bearerToken);
|
||||
}
|
||||
if (this.method === 'POST' || this.method === 'PUT') {
|
||||
const requestBody = this.rawRequestBody
|
||||
const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody;
|
||||
xhr.setRequestHeader('Content-Length', requestBody.length)
|
||||
xhr.setRequestHeader('Content-Type', `${this.contentType}; charset=utf-8`)
|
||||
xhr.send(requestBody)
|
||||
@@ -337,13 +349,26 @@
|
||||
xhr.send()
|
||||
}
|
||||
xhr.onload = e => {
|
||||
this.response.status = xhr.status
|
||||
const headers = this.response.headers = parseHeaders(xhr)
|
||||
if ((headers['content-type'] || '').startsWith('application/json')) {
|
||||
this.response.body = JSON.stringify(JSON.parse(xhr.responseText), null, 2)
|
||||
} else {
|
||||
this.response.body = xhr.responseText
|
||||
}
|
||||
this.response.status = xhr.status
|
||||
const headers = this.response.headers = parseHeaders(xhr)
|
||||
if ((headers['content-type'] || '').startsWith('application/json')) {
|
||||
this.response.body = JSON.stringify(JSON.parse(xhr.responseText), null, 2)
|
||||
} else {
|
||||
this.response.body = xhr.responseText
|
||||
}
|
||||
|
||||
if (!this.isValidURL) {
|
||||
alert('Please check the formatting of the URL');
|
||||
return
|
||||
}
|
||||
const n = new Date().toLocaleTimeString()
|
||||
this.history = [{
|
||||
status: xhr.status,
|
||||
time: n,
|
||||
method: this.method,
|
||||
url: this.url,
|
||||
path: this.path
|
||||
}, ...this.history]
|
||||
}
|
||||
xhr.onerror = e => {
|
||||
this.response.status = xhr.status
|
||||
@@ -369,6 +394,36 @@
|
||||
},
|
||||
removeRequestBodyParam(index) {
|
||||
this.bodyParams.splice(index, 1)
|
||||
},
|
||||
formatRawParams(event) {
|
||||
if ((event.which !== 13 && event.which !== 9)) {
|
||||
return;
|
||||
}
|
||||
const textBody = event.target.value;
|
||||
const textBeforeCursor = textBody.substring(0, event.target.selectionStart);
|
||||
const textAfterCursor = textBody.substring(event.target.selectionEnd);
|
||||
|
||||
if (event.which === 13) {
|
||||
event.preventDefault();
|
||||
const oldSelectionStart = event.target.selectionStart;
|
||||
const lastLine = textBeforeCursor.split('\n').slice(-1)[0];
|
||||
const rightPadding = lastLine.match(/([\s\t]*).*/)[1] || "";
|
||||
event.target.value = textBeforeCursor + '\n' + rightPadding + textAfterCursor;
|
||||
setTimeout(() => event.target.selectionStart = event.target.selectionEnd = oldSelectionStart + rightPadding.length + 1, 1);
|
||||
}
|
||||
else if (event.which === 9) {
|
||||
event.preventDefault();
|
||||
const oldSelectionStart = event.target.selectionStart;
|
||||
event.target.value = textBeforeCursor + '\xa0\xa0' + textAfterCursor;
|
||||
event.target.selectionStart = event.target.selectionEnd = oldSelectionStart + 2;
|
||||
return false;
|
||||
}
|
||||
|
||||
},
|
||||
copyResponse() {
|
||||
var copyText = document.getElementById("response-details");
|
||||
copyText.select();
|
||||
document.execCommand("copy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<label for="url">URL</label>
|
||||
<input id="url" type="url" :class="{ error: !urlValid }" v-model="url" @keyup.enter="toggleConnection">
|
||||
</li>
|
||||
<li class="no-grow">
|
||||
<li>
|
||||
<label> </label>
|
||||
<button class="action" :class="{ disabled: !urlValid }" name="action" @click="toggleConnection">{{ toggleConnectionVerb }}</button>
|
||||
<button :class="{ disabled: !urlValid }" name="action" @click="toggleConnection">{{ toggleConnectionVerb }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
@@ -18,7 +18,7 @@
|
||||
<ul>
|
||||
<li>
|
||||
<label for="log">Log</label>
|
||||
<div id="log" name="log" class="log" readonly>
|
||||
<div id="log" name="log" class="log">
|
||||
<span v-if="communication.log">
|
||||
<span v-for="logEntry in communication.log" :style="{ color: logEntry.color }">{{ getSourcePrefix(logEntry.source) }} {{ logEntry.payload }}</span>
|
||||
</span>
|
||||
@@ -33,9 +33,9 @@
|
||||
<input id="message" name="message" type="text" v-model="communication.input" :readonly="!connectionState" @keyup.enter="sendMessage">
|
||||
</li>
|
||||
|
||||
<li class="no-grow">
|
||||
<li>
|
||||
<label> </label>
|
||||
<button class="action" name="send" :class="{ disabled: !connectionState }" @click="sendMessage">Send</button>
|
||||
<button name="send" :class="{ disabled: !connectionState }" @click="sendMessage">Send</button>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
@@ -44,13 +44,6 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.no-grow { flex-grow: 0; }
|
||||
.action {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
div.log {
|
||||
margin: 4px;
|
||||
padding: 8px 16px;
|
||||
@@ -58,7 +51,7 @@
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-dark-color);
|
||||
color: var(--fg-color);
|
||||
height: 300px;
|
||||
height: 256px;
|
||||
overflow: auto;
|
||||
|
||||
&, span {
|
||||
|
||||
Reference in New Issue
Block a user