Merge remote-tracking branch 'upstream/master'

This commit is contained in:
izerozlu
2019-08-26 18:25:53 +03:00
8 changed files with 181 additions and 104 deletions

View File

@@ -35,10 +35,10 @@ notifications:
deploy: deploy:
provider: pages provider: pages
skip_cleanup: true skip-cleanup: true
# Refer to: https://docs.travis-ci.com/user/deployment/pages/#Setting-the-GitHub-token # 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 target-branch: gh-pages
local_dir: dist local-dir: dist
on: on:
branch: master branch: master

View File

@@ -13,7 +13,7 @@ When I wrote this, only God and I understood what I was doing. Now, only God kno
--- ---
[![Build Status](https://travis-ci.org/liyasthomas/postwoman.svg?branch=master)](https://travis-ci.org/liyasthomas/postwoman) [![GitHub release](https://img.shields.io/github/release/liyasthomas/postwoman/all.svg)](https://github.com/liyasthomas/postwoman/releases/latest) [![repo size](https://img.shields.io/github/repo-size/liyasthomas/postwoman.svg)](https://github.com/liyasthomas/postwoman/archive/master.zip) [![license](https://img.shields.io/github/license/liyasthomas/postwoman.svg)](https://github.com/liyasthomas/postwoman/blob/master/LICENSE) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/liyasthomas/postwoman/issues) [![Website](https://img.shields.io/website-up-down-green-red/https/shields.io.svg?label=website)](https://liyas-thomas.firebaseapp.com) [![Donate](https://img.shields.io/badge/$-donate-ff69b4.svg)](https://www.paypal.me/liyascthomas) [![Buy me a coffee](https://img.shields.io/badge/$-BuyMeACoffee-orange.svg)](https://www.buymeacoffee.com/liyasthomas) [![Build Status](https://travis-ci.com/liyasthomas/postwoman.svg?branch=master)](https://travis-ci.com/liyasthomas/postwoman) [![GitHub release](https://img.shields.io/github/release/liyasthomas/postwoman/all.svg)](https://github.com/liyasthomas/postwoman/releases/latest) [![repo size](https://img.shields.io/github/repo-size/liyasthomas/postwoman.svg)](https://github.com/liyasthomas/postwoman/archive/master.zip) [![license](https://img.shields.io/github/license/liyasthomas/postwoman.svg)](https://github.com/liyasthomas/postwoman/blob/master/LICENSE) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/liyasthomas/postwoman/issues) [![Website](https://img.shields.io/website-up-down-green-red/https/shields.io.svg?label=website)](https://liyas-thomas.firebaseapp.com) [![Donate](https://img.shields.io/badge/$-donate-ff69b4.svg)](https://www.paypal.me/liyascthomas) [![Buy me a coffee](https://img.shields.io/badge/$-BuyMeACoffee-orange.svg)](https://www.buymeacoffee.com/liyasthomas)
# <img src="static/icon.png" alt="postwoman" width="32"> Postwoman # <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 ## 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) * [Liyas Thomas](https://github.com/liyasthomas)
### Contributors ### Contributors
* [NBTX](https://github.com/NBTX)
* [Andrew Bastin](https://github.com/AndrewBastin) * [Andrew Bastin](https://github.com/AndrewBastin)
* [Nick Palenchar](https://github.com/nickpalenchar)
* [Abraham Williams](https://github.com/abraham) * [Abraham Williams](https://github.com/abraham)
* [Nicholas La Roux](https://github.com/larouxn)
* [RifqiAlAbqary](https://github.com/reefqi037)
* [izerozlu](https://github.com/izerozlu)
### Thanks ### Thanks
* [Dribbble](https://dribbble.com) * [Dribbble](https://dribbble.com)

View File

@@ -234,6 +234,16 @@ ol li {
flex-direction: column; flex-direction: column;
flex-grow: 1; 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) { @media (max-width: $responsiveWidth) {
ul, ul,
@@ -252,21 +262,34 @@ ol li {
} }
.info-response { .info-response {
background-color: #ffeb3b; background-color: #FFEB3B;
} }
.success-response { .success-response {
background-color: #66BB6A; background-color: #4BB543;
} }
.redir-response { .redir-response {
background-color: #ff5722; background-color: #FF5722;
} }
.cl-error-response { .cl-error-response {
background-color: #ef5350; background-color: #A63232;
} }
.sv-error-response { .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;
}
}
}

View File

@@ -1,5 +1,5 @@
<template> <template>
<fieldset :class="{ 'no-colored-frames': noFrameColors }"> <fieldset :id="label.toLowerCase()" :class="{ 'no-colored-frames': noFrameColors }">
<legend @click.prevent="collapse">{{ label }} </legend> <legend @click.prevent="collapse">{{ label }} </legend>
<div class="collapsible" :class="{ hidden: collapsed }"> <div class="collapsible" :class="{ hidden: collapsed }">
<slot /> <slot />
@@ -44,4 +44,4 @@
} }
} }
</script> </script>

View File

@@ -5,7 +5,7 @@
<nuxt-link to="/"> <nuxt-link to="/">
<h1 class="logo"><logo alt="" style="height: 24px; margin-right: 16px"/>Postwoman</h1> <h1 class="logo"><logo alt="" style="height: 24px; margin-right: 16px"/>Postwoman</h1>
</nuxt-link> </nuxt-link>
<h3>Lightweight API request builder</h3> <h3>API request builder</h3>
</div> </div>
<nav> <nav>

View File

@@ -3,7 +3,7 @@
// TODO: Use these when rendering the pages (rather than just for head/meta tags...) // TODO: Use these when rendering the pages (rather than just for head/meta tags...)
export const meta = { export const meta = {
name: "Postwoman", 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." 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' }, { charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no, minimal-ui' }, { 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 || '' }, { 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" }, { name: 'X-UA-Compatible', content: "IE=edge, chrome=1" },
{ itemprop: "name", content: `${meta.name} \u2022 ${meta.shortDescription}` }, { itemprop: "name", content: `${meta.name} \u2022 ${meta.shortDescription}` },

View File

@@ -36,34 +36,42 @@
<option>application/json</option> <option>application/json</option>
<option>www-form/urlencoded</option> <option>www-form/urlencoded</option>
</select> </select>
<span>
<input v-model="rawInput" style="cursor: pointer;" type="checkbox" id="rawInput">
<label for="rawInput" style="cursor: pointer;">Raw Input</label>
</span>
</li> </li>
</ul> </ul>
<ol v-for="(param, index) in bodyParams"> <div v-if="!rawInput">
<li> <ol v-for="(param, index) in bodyParams">
<label :for="'bparam'+index">Key {{index + 1}}</label> <li>
<input :name="'bparam'+index" v-model="param.key"> <label :for="'bparam'+index">Key {{index + 1}}</label>
</li> <input :name="'bparam'+index" v-model="param.key">
<li> </li>
<label :for="'bvalue'+index">Value {{index + 1}}</label> <li>
<input :name="'bvalue'+index" v-model="param.value"> <label :for="'bvalue'+index">Value {{index + 1}}</label>
</li> <input :name="'bvalue'+index" v-model="param.value">
<li> </li>
<label for="request">&nbsp;</label> <li>
<button name="request" @click="removeRequestBodyParam(index)">Remove</button> <label for="request">&nbsp;</label>
</li> <button name="request" @click="removeRequestBodyParam(index)">Remove</button>
</ol> </li>
<ul> </ol>
<li> <ul>
<label for="addrequest">Action</label> <li>
<button name="addrequest" @click="addRequestBodyParam">Add</button> <label for="addrequest">Action</label>
</li> <button name="addrequest" @click="addRequestBodyParam">Add</button>
</ul> </li>
<ul> </ul>
<li> <ul>
<label for="request">Parameter List</label> <li>
<textarea name="request" rows="1" readonly>{{rawRequestBody || '(add at least one parameter)'}}</textarea> <label for="request">Parameter List</label>
</li> <textarea name="request" rows="1" readonly>{{rawRequestBody || '(add at least one parameter)'}}</textarea>
</ul> </li>
</ul>
</div><div v-else>
<textarea v-model="rawParams" style="font-family: monospace;" rows="16" @keydown="formatRawParams"></textarea>
</div>
</pw-section> </pw-section>
<pw-section class="green" label="Authentication" collapsed> <pw-section class="green" label="Authentication" collapsed>
@@ -138,9 +146,12 @@
</li> </li>
</ul> </ul>
<ul> <ul>
<li> <li>
<div class="flex-wrap">
<label for="body">response</label> <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> </li>
</ul> </ul>
</pw-section> </pw-section>
@@ -156,17 +167,19 @@
<label for="time">Time</label> <label for="time">Time</label>
<input name="time" type="text" readonly :value="entry.time"> <input name="time" type="text" readonly :value="entry.time">
</li> </li>
<li> <li class="method-list-item">
<label for="name">Method</label> <label for="method">Method</label>
<input name="name" type="text" readonly :value="entry.method"> <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>
<li> <li>
<label for="name">URL</label> <label for="url">URL</label>
<input name="name" type="text" readonly :value="entry.url"> <input name="url" type="text" readonly :value="entry.url">
</li> </li>
<li> <li>
<label for="name">Path</label> <label for="path">Path</label>
<input name="name" type="text" readonly :value="entry.path"> <input name="path" type="text" readonly :value="entry.path">
</li> </li>
<li> <li>
<label for="delete">&nbsp;</label> <label for="delete">&nbsp;</label>
@@ -183,22 +196,33 @@
</template> </template>
<script> <script>
const parseHeaders = xhr => { const statusCategories = [
const headers = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/); {name: 'informational', statusCodeRegex: new RegExp(/[1][0-9]+/), className: 'info-response'},
const headerMap = {}; {name: 'successful', statusCodeRegex: new RegExp(/[2][0-9]+/), className: 'success-response'},
headers.forEach(line => { {name: 'redirection', statusCodeRegex: new RegExp(/[3][0-9]+/), className: 'redir-response'},
const parts = line.split(': '); {name: 'client error', statusCodeRegex: new RegExp(/[4][0-9]+/), className: 'cl-error-response'},
const header = parts.shift().toLowerCase(); {name: 'server error', statusCodeRegex: new RegExp(/[5][0-9]+/), className: 'sv-error-response'},
const value = parts.join(': '); ];
headerMap[header] = value
});
return headerMap
};
import section from "../components/section";
export default { const parseHeaders = xhr => {
components: { 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 'pw-section': section
}, },
@@ -213,6 +237,8 @@
bearerToken: '', bearerToken: '',
params: [], params: [],
bodyParams: [], bodyParams: [],
rawParams: '',
rawInput: false,
contentType: 'application/json', contentType: 'application/json',
response: { response: {
status: '', status: '',
@@ -224,13 +250,7 @@
}, },
computed: { computed: {
statusCategory(){ statusCategory(){
return [ return findStatusGroup(this.response.status);
{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));
}, },
noHistoryToClear() { noHistoryToClear() {
return this.history.length === 0; return this.history.length === 0;
@@ -280,6 +300,9 @@
} }
}, },
methods: { methods: {
findEntryStatus(entry){
return findStatusGroup(entry.status);
},
deleteHistory(entry) { deleteHistory(entry) {
this.history.splice(this.history.indexOf(entry), 1) this.history.splice(this.history.indexOf(entry), 1)
window.localStorage.setItem('history', JSON.stringify(this.history)) window.localStorage.setItem('history', JSON.stringify(this.history))
@@ -301,17 +324,6 @@
}) })
}, },
sendRequest() { 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)) window.localStorage.setItem('history', JSON.stringify(this.history))
if (this.$refs.response.$el.classList.contains('hidden')) { if (this.$refs.response.$el.classList.contains('hidden')) {
this.$refs.response.$el.classList.toggle('hidden') this.$refs.response.$el.classList.toggle('hidden')
@@ -329,7 +341,7 @@
xhr.setRequestHeader('Authorization', 'Bearer ' + this.bearerToken); xhr.setRequestHeader('Authorization', 'Bearer ' + this.bearerToken);
} }
if (this.method === 'POST' || this.method === 'PUT') { 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-Length', requestBody.length)
xhr.setRequestHeader('Content-Type', `${this.contentType}; charset=utf-8`) xhr.setRequestHeader('Content-Type', `${this.contentType}; charset=utf-8`)
xhr.send(requestBody) xhr.send(requestBody)
@@ -337,13 +349,26 @@
xhr.send() xhr.send()
} }
xhr.onload = e => { xhr.onload = e => {
this.response.status = xhr.status this.response.status = xhr.status
const headers = this.response.headers = parseHeaders(xhr) const headers = this.response.headers = parseHeaders(xhr)
if ((headers['content-type'] || '').startsWith('application/json')) { if ((headers['content-type'] || '').startsWith('application/json')) {
this.response.body = JSON.stringify(JSON.parse(xhr.responseText), null, 2) this.response.body = JSON.stringify(JSON.parse(xhr.responseText), null, 2)
} else { } else {
this.response.body = xhr.responseText 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 => { xhr.onerror = e => {
this.response.status = xhr.status this.response.status = xhr.status
@@ -369,6 +394,36 @@
}, },
removeRequestBodyParam(index) { removeRequestBodyParam(index) {
this.bodyParams.splice(index, 1) 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");
} }
} }
} }

View File

@@ -7,9 +7,9 @@
<label for="url">URL</label> <label for="url">URL</label>
<input id="url" type="url" :class="{ error: !urlValid }" v-model="url" @keyup.enter="toggleConnection"> <input id="url" type="url" :class="{ error: !urlValid }" v-model="url" @keyup.enter="toggleConnection">
</li> </li>
<li class="no-grow"> <li>
<label>&nbsp;</label> <label>&nbsp;</label>
<button class="action" :class="{ disabled: !urlValid }" name="action" @click="toggleConnection">{{ toggleConnectionVerb }}</button> <button :class="{ disabled: !urlValid }" name="action" @click="toggleConnection">{{ toggleConnectionVerb }}</button>
</li> </li>
</ul> </ul>
</pw-section> </pw-section>
@@ -18,7 +18,7 @@
<ul> <ul>
<li> <li>
<label for="log">Log</label> <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-if="communication.log">
<span v-for="logEntry in communication.log" :style="{ color: logEntry.color }">{{ getSourcePrefix(logEntry.source) }} {{ logEntry.payload }}</span> <span v-for="logEntry in communication.log" :style="{ color: logEntry.color }">{{ getSourcePrefix(logEntry.source) }} {{ logEntry.payload }}</span>
</span> </span>
@@ -33,9 +33,9 @@
<input id="message" name="message" type="text" v-model="communication.input" :readonly="!connectionState" @keyup.enter="sendMessage"> <input id="message" name="message" type="text" v-model="communication.input" :readonly="!connectionState" @keyup.enter="sendMessage">
</li> </li>
<li class="no-grow"> <li>
<label>&nbsp;</label> <label>&nbsp;</label>
<button class="action" name="send" :class="{ disabled: !connectionState }" @click="sendMessage">Send</button> <button name="send" :class="{ disabled: !connectionState }" @click="sendMessage">Send</button>
</li> </li>
</ul> </ul>
</pw-section> </pw-section>
@@ -44,13 +44,6 @@
</template> </template>
<style lang="scss"> <style lang="scss">
.no-grow { flex-grow: 0; }
.action {
padding-left: 30px;
padding-right: 30px;
width: 150px;
}
div.log { div.log {
margin: 4px; margin: 4px;
padding: 8px 16px; padding: 8px 16px;
@@ -58,7 +51,7 @@
border-radius: 4px; border-radius: 4px;
background-color: var(--bg-dark-color); background-color: var(--bg-dark-color);
color: var(--fg-color); color: var(--fg-color);
height: 300px; height: 256px;
overflow: auto; overflow: auto;
&, span { &, span {