move the button to retry on the list

This commit is contained in:
Simon Vieille 2023-05-26 14:19:05 +02:00
parent c1fdabeb2c
commit 957e3ed211
Signed by: deblan
GPG key ID: 579388D585F70417

View file

@ -1,441 +1,458 @@
<!DOCTYPE html> <!DOCTYPE html>
<html ng-app="app"> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" href="data:;base64,iVBORw0KGgo="> <link rel="icon" href="data:;base64,iVBORw0KGgo=">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<link href="https://fonts.googleapis.com/css?family=Inconsolata:400,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Inconsolata:400,700" rel="stylesheet">
<title>Capture</title> <title>Capture</title>
<style> <style>
[v-cloak] { [v-cloak] {
display: none !important; display: none !important;
} }
:root { :root {
--bg: #282c34; --bg: #282c34;
--list-item-bg: #2c313a; --list-item-bg: #2c313a;
--list-item-fg: #abb2bf; --list-item-fg: #abb2bf;
--list-item-sel-bg: hsl(219, 22%, 25%); --list-item-sel-bg: hsl(219, 22%, 25%);
--req-res-bg: #2c313a; --req-res-bg: #2c313a;
--req-res-fg: #abb2bf; --req-res-fg: #abb2bf;
--links: #55b5c1; --links: #55b5c1;
--method-get: #98c379; --method-get: #98c379;
--method-post: #c678dd; --method-post: #c678dd;
--method-put: #d19a66; --method-put: #d19a66;
--method-patch: #a7afbc; --method-patch: #a7afbc;
--method-delete: #e06c75; --method-delete: #e06c75;
--status-ok: #98c379; --status-ok: #98c379;
--status-warn: #d19a66; --status-warn: #d19a66;
--status-error: #e06c75; --status-error: #e06c75;
--btn-bg: var(--list-item-bg); --btn-bg: var(--list-item-bg);
--btn-hover: var(--list-item-sel-bg); --btn-hover: var(--list-item-sel-bg);
--disabled: hsl(187, 5%, 50%); --disabled: hsl(187, 5%, 50%);
} }
* { * {
padding: 0; padding: 0;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
} }
div { div {
position: relative; position: relative;
} }
html, html,
body, body,
.dashboard { .dashboard {
height: 100%; height: 100%;
font-family: 'Inconsolata', monospace; font-family: 'Inconsolata', monospace;
font-size: 1em; font-size: 1em;
font-weight: 400; font-weight: 400;
background: var(--bg); background: var(--bg);
} }
.dashboard { .dashboard {
display: grid; display: grid;
grid-template-columns: .6fr 1fr 1fr; grid-template-columns: .6fr 1fr 1fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
gap: .5rem; gap: .5rem;
} }
.list, .list,
.req, .req,
.res { .res {
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
gap: .5rem; gap: .5rem;
} }
body { body {
padding: .5rem; padding: .5rem;
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: .25rem; width: .25rem;
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
background-color: var(--list-item-fg); background-color: var(--list-item-fg);
} }
.list, .list,
.req, .req,
.res { .res {
overflow: auto; overflow: auto;
} }
.list-inner, .list-inner,
.req-inner, .req-inner,
.res-inner { .res-inner {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
} }
.req-inner, .req-inner,
.res-inner { .res-inner {
background: var(--req-res-bg); background: var(--req-res-bg);
} }
.req, .req,
.res { .res {
color: var(--req-res-fg); color: var(--req-res-fg);
} }
.list-inner { .list-inner {
display: grid; display: grid;
grid-template-rows: auto; grid-template-rows: auto;
gap: .5rem; gap: .5rem;
align-content: start; align-content: start;
} }
.list-item { .list-item {
display: grid; display: grid;
grid-template-columns: auto 1fr auto auto; grid-template-columns: auto 1fr auto auto auto;
gap: .5rem; gap: .5rem;
font-size: 1.2em; font-size: 1.2em;
padding: 1rem; padding: 1rem;
background: var(--list-item-bg); background: var(--list-item-bg);
color: var(--list-item-fg); color: var(--list-item-fg);
cursor: pointer; cursor: pointer;
transition: background .15s linear; transition: background .15s linear;
} }
.list-item, .list-item,
.req, .req,
.res { .res {
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.1); box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.1);
} }
.list-item.selected { .list-item.selected {
background: var(--list-item-sel-bg); background: var(--list-item-sel-bg);
} }
.GET { .GET {
color: var(--method-get); color: var(--method-get);
} }
.POST { .POST {
color: var(--method-post); color: var(--method-post);
} }
.PUT { .PUT {
color: var(--method-put); color: var(--method-put);
} }
.PATCH { .PATCH {
color: var(--method-patch); color: var(--method-patch);
} }
.DELETE { .DELETE {
color: var(--method-delete); color: var(--method-delete);
} }
.ok { .ok {
color: var(--status-ok); color: var(--status-ok);
} }
.warn { .warn {
color: var(--status-warn); color: var(--status-warn);
} }
.error { .error {
color: var(--status-error); color: var(--status-error);
} }
.method { .method {
font-size: 0.7em; font-size: 0.7em;
} }
.status { .status {
font-size: 0.8em; font-size: 0.8em;
} }
.path { .path {
font-size: 0.8em; font-size: 0.8em;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
direction: rtl; direction: rtl;
} }
.time { .time {
font-size: 0.7em; font-size: 0.7em;
color: var(--disabled); color: var(--disabled);
} }
pre { pre {
word-break: break-all; word-break: break-all;
white-space: pre-wrap; white-space: pre-wrap;
padding: 1rem; padding: 1rem;
font-family: inherit; font-family: inherit;
font-weight: 400; font-weight: 400;
line-height: 1.2em; line-height: 1.2em;
} }
.corner { .corner {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
width: 80px; width: 80px;
height: 50px; height: 50px;
background: var(--bg); background: var(--bg);
color: var(--disabled); color: var(--disabled);
display: grid; display: grid;
align-content: end; align-content: end;
justify-content: center; justify-content: center;
transform: rotate(45deg) translate(10px, -40px); transform: rotate(45deg) translate(10px, -40px);
padding-bottom: 4px; padding-bottom: 4px;
font-size: .8em; font-size: .8em;
user-select: none; user-select: none;
} }
.controls { .controls {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
gap: .5rem; gap: .5rem;
justify-content: start; justify-content: start;
} }
button { button {
background: var(--btn-bg); background: var(--btn-bg);
border: 0; border: 0;
padding: .5rem 1rem; padding: .5rem 1rem;
font-size: .75em; font-size: .75em;
font-family: inherit; font-family: inherit;
color: var(--links); color: var(--links);
cursor: pointer; cursor: pointer;
outline: 0; outline: 0;
} }
button:disabled { button:disabled {
color: var(--disabled); color: var(--disabled);
cursor: default; cursor: default;
} }
button:hover:enabled { button:hover:enabled {
background: var(--btn-hover); background: var(--btn-hover);
} }
.welcome { .retry {
display: grid; padding: 0;
position: absolute; }
background: rgba(0, 0, 0, .5);
justify-content: center;
line-height: 1.5rem;
z-index: 9;
color: #fff;
font-size: 2em;
top: 50%;
right: 1rem;
left: 1rem;
transform: translate(0%, -50%);
padding: 3rem;
box-shadow: 0px 0px 20px 10px rgba(0, 0, 0, 0.1);
word-break: break-word;
}
.welcome span { .retry svg {
font-size: .5em; width: 16px;
color: #999; height: 16px;
} stroke: #9a9996;
}
@media only screen and (max-width: 1024px) { .retry:hover svg * {
.dashboard { stroke: #fff;
grid-template-columns: .7fr 1fr; }
grid-template-rows: 1fr 1fr;
}
.list { .welcome {
grid-row: 1 / 3; display: grid;
} position: absolute;
background: rgba(0, 0, 0, .5);
justify-content: center;
line-height: 1.5rem;
z-index: 9;
color: #fff;
font-size: 2em;
top: 50%;
right: 1rem;
left: 1rem;
transform: translate(0%, -50%);
padding: 3rem;
box-shadow: 0px 0px 20px 10px rgba(0, 0, 0, 0.1);
word-break: break-word;
}
.req { .welcome span {
grid-column: 2; font-size: .5em;
} color: #999;
}
.res { @media only screen and (max-width: 1024px) {
grid-column: 2; .dashboard {
grid-row: 2; grid-template-columns: .7fr 1fr;
} grid-template-rows: 1fr 1fr;
}
.welcome { .list {
font-size: 1.5em; grid-row: 1 / 3;
} }
}
@media only screen and (max-width: 484px) { .req {
.dashboard { grid-column: 2;
grid-template-columns: 1fr; }
grid-template-rows: 1fr 1fr 1fr;
column-gap: 0;
}
.list { .res {
grid-area: 1 / 2; grid-column: 2;
} grid-row: 2;
}
.req { .welcome {
grid-row: 2; font-size: 1.5em;
} }
}
.res { @media only screen and (max-width: 484px) {
grid-row: 3; .dashboard {
} grid-template-columns: 1fr;
} grid-template-rows: 1fr 1fr 1fr;
</style> column-gap: 0;
}
.list {
grid-area: 1 / 2;
}
.req {
grid-row: 2;
}
.res {
grid-row: 3;
}
}
</style>
</head> </head>
<body> <body>
<div class="dashboard" id="app" v-cloak>
<div class="list">
<div class="controls">
<button :disabled="items.length == 0" @click="clearDashboard">clear</button>
</div>
<div class="list-inner">
<div class="list-item" v-for="item in items" :key="item.id" @click="show(item)"
:class="{selected: selectedItem.id == item.id}">
<span class="method" :class="item.method">{{item.method}}</span>
<span class="path">&lrm;{{item.path}}&lrm;</span>
<span class="time">{{item.elapsed}}ms</span>
<span class="status" :class="statusColor(item)">
{{item.status == 999 ? 'failed' : item.status}}
</span>
<button class="retry" @click="retry(item.id)">
<svg stroke-width="3" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21.888 13.5C21.164 18.311 17.013 22 12 22 6.477 22 2 17.523 2 12S6.477 2 12 2c4.1 0 7.625 2.468 9.168 6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path><path d="M17 8h4.4a.6.6 0 00.6-.6V3" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</button>
</div>
</div>
</div>
<div class="dashboard" id="app" v-cloak> <div class="req">
<div class="controls">
<button :disabled="!canPrettifyBody('request')" @click="prettifyBody('request')">prettify</button>
<button :disabled="selectedItem.id == null" @click="copyCurl($event)" data-text="curl">curl</button>
</div>
<div class="req-inner">
<div class="corner">req</div>
<pre>{{selectedItem.request}}</pre>
</div>
</div>
<div class="list"> <div class="res">
<div class="controls"> <div class="controls">
<button :disabled="items.length == 0" @click="clearDashboard">clear</button> <button :disabled="!canPrettifyBody('response')" @click="prettifyBody('response')">prettify</button>
</div> </div>
<div class="list-inner"> <div class="res-inner">
<div class="list-item" v-for="item in items" :key="item.id" @click="show(item)" <div class="corner">res</div>
:class="{selected: selectedItem.id == item.id}"> <pre :class="{error: selectedItem.status == 999}">{{selectedItem.response}}</pre>
<span class="method" :class="item.method">{{item.method}}</span> </div>
<span class="path">&lrm;{{item.path}}&lrm;</span> </div>
<span class="time">{{item.elapsed}}ms</span>
<span class="status" :class="statusColor(item)">{{item.status == 999 ? 'failed' :
item.status}}</span>
</div>
</div>
</div>
<div class="req"> <div class="welcome" v-show="items.length == 0">
<div class="controls"> <p>
<button :disabled="!canPrettifyBody('request')" @click="prettifyBody('request')">prettify</button> Waiting for requests on http://localhost:{{proxyPort}}/<br>
<button :disabled="selectedItem.id == null" @click="copyCurl($event)" data-text="curl">curl</button> <span>Proxying {{targetURL}}</span>
<button :disabled="selectedItem.id == null" @click="retry">retry</button> </p>
</div> </div>
<div class="req-inner">
<div class="corner">req</div>
<pre>{{selectedItem.request}}</pre>
</div>
</div>
<div class="res"> </div>
<div class="controls">
<button :disabled="!canPrettifyBody('response')" @click="prettifyBody('response')">prettify</button>
</div>
<div class="res-inner">
<div class="corner">res</div>
<pre :class="{error: selectedItem.status == 999}">{{selectedItem.response}}</pre>
</div>
</div>
<div class="welcome" v-show="items.length == 0"> <script type="text/javascript">
<p>Waiting for requests on http://localhost:{{proxyPort}}/<br> var app = new Vue({
<span>Proxying {{targetURL}}</span> el: '#app',
</p> data: {
</div> items: [],
selectedItem: {},
</div> proxyPort: '',
targetURL: '',
<script type="text/javascript"> },
var app = new Vue({ created() {
el: '#app', this.setupStream();
data: { },
items: [], methods: {
selectedItem: {}, setupStream() {
proxyPort: '', let es = new EventSource('/conn/');
targetURL: '', es.addEventListener('config', event => {
}, const cfg = JSON.parse(event.data);
created() { this.proxyPort = cfg.ProxyPort;
this.setupStream(); this.targetURL = cfg.TargetURL;
}, });
methods: { es.addEventListener('captures', event => {
setupStream() { this.items = JSON.parse(event.data).reverse();
let es = new EventSource('/conn/'); });
es.addEventListener('config', event => { es.onerror = () => {
const cfg = JSON.parse(event.data); this.items = [];
this.proxyPort = cfg.ProxyPort; this.selectedItem = {};
this.targetURL = cfg.TargetURL; };
}); },
es.addEventListener('captures', event => { async show(item) {
this.items = JSON.parse(event.data).reverse(); this.selectedItem = { ...this.selectedItem, id: item.id, status: item.status };
}); let resp = await fetch('/info/' + item.id);
es.onerror = () => { let data = await resp.json();
this.items = []; this.selectedItem = { ...this.selectedItem, ...data };
this.selectedItem = {}; },
}; statusColor(item) {
}, if (item.status < 300) return 'ok';
async show(item) { if (item.status < 400) return 'warn';
this.selectedItem = { ...this.selectedItem, id: item.id, status: item.status }; return 'error';
let resp = await fetch('/info/' + item.id); },
let data = await resp.json(); async clearDashboard() {
this.selectedItem = { ...this.selectedItem, ...data }; this.selectedItem = {};
}, await fetch('/clear/');
statusColor(item) { },
if (item.status < 300) return 'ok'; canPrettifyBody(name) {
if (item.status < 400) return 'warn'; if (!this.selectedItem[name]) return false;
return 'error'; return this.selectedItem[name].indexOf('Content-Type: application/json') != -1;
}, },
async clearDashboard() { prettifyBody(key) {
this.selectedItem = {}; let regex = /\n([\{\[](.*\s*)*[\}\]])/;
await fetch('/clear/'); let data = this.selectedItem[key];
}, let match = regex.exec(data);
canPrettifyBody(name) { let body = match[1];
if (!this.selectedItem[name]) return false; let prettyBody = JSON.stringify(JSON.parse(body), null, ' ');
return this.selectedItem[name].indexOf('Content-Type: application/json') != -1; this.selectedItem[key] = data.replace(body, prettyBody);
}, },
prettifyBody(key) { copyCurl(event) {
let regex = /\n([\{\[](.*\s*)*[\}\]])/; this.changeText(event);
let data = this.selectedItem[key]; let e = document.createElement('textarea');
let match = regex.exec(data); e.value = this.selectedItem.curl;
let body = match[1]; document.body.appendChild(e);
let prettyBody = JSON.stringify(JSON.parse(body), null, ' '); e.select();
this.selectedItem[key] = data.replace(body, prettyBody); document.execCommand('copy');
}, document.body.removeChild(e);
copyCurl(event) { },
this.changeText(event); async retry(id) {
let e = document.createElement('textarea'); await fetch(`/retry/${id}`, {
e.value = this.selectedItem.curl; headers: {
document.body.appendChild(e); 'Cache-Control': 'no-cache'
e.select(); }
document.execCommand('copy'); });
document.body.removeChild(e); this.show(this.items[0]);
}, },
async retry() { changeText(event) {
await fetch('/retry/' + this.selectedItem.id, let elem = event.target;
{ headers: { 'Cache-Control': 'no-cache' } }); let btnText = elem.getAttribute("data-text");
this.show(this.items[0]); elem.innerText = "copied!";
}, setTimeout(() => elem.innerText = btnText, 400)
changeText(event) { }
let elem = event.target; },
let btnText = elem.getAttribute("data-text"); });
elem.innerText = "copied!"; </script>
setTimeout(() => elem.innerText = btnText, 400)
}
},
});
</script>
</body> </body>
</html>
</html>