mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 13:35:54 +00:00
401 lines
21 KiB
HTML
401 lines
21 KiB
HTML
<html>
|
|
<head>
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
|
|
<script>
|
|
!function (a, b) {
|
|
"function" == typeof define && define.amd ? define([], b) : "undefined" != typeof module && module.exports ? module.exports = b() : a.ReconnectingWebSocket = b()
|
|
}(this, function () {
|
|
function a(b, c, d) {
|
|
function l(a, b) {
|
|
var c = document.createEvent("CustomEvent");
|
|
return c.initCustomEvent(a, !1, !1, b), c
|
|
}
|
|
|
|
var e = {
|
|
debug: !1,
|
|
automaticOpen: !0,
|
|
reconnectInterval: 1e3,
|
|
maxReconnectInterval: 3e4,
|
|
reconnectDecay: 1.5,
|
|
timeoutInterval: 2e3
|
|
};
|
|
d || (d = {});
|
|
for (var f in e) this[f] = "undefined" != typeof d[f] ? d[f] : e[f];
|
|
this.url = b, this.reconnectAttempts = 0, this.readyState = WebSocket.CONNECTING, this.protocol = null;
|
|
var h, g = this, i = !1, j = !1, k = document.createElement("div");
|
|
k.addEventListener("open", function (a) {
|
|
g.onopen(a)
|
|
}), k.addEventListener("close", function (a) {
|
|
g.onclose(a)
|
|
}), k.addEventListener("connecting", function (a) {
|
|
g.onconnecting(a)
|
|
}), k.addEventListener("message", function (a) {
|
|
g.onmessage(a)
|
|
}), k.addEventListener("error", function (a) {
|
|
g.onerror(a)
|
|
}), this.addEventListener = k.addEventListener.bind(k), this.removeEventListener = k.removeEventListener.bind(k), this.dispatchEvent = k.dispatchEvent.bind(k), this.open = function (b) {
|
|
h = new WebSocket(g.url, c || []), b || k.dispatchEvent(l("connecting")), (g.debug || a.debugAll) && console.debug("ReconnectingWebSocket", "attempt-connect", g.url);
|
|
var d = h, e = setTimeout(function () {
|
|
(g.debug || a.debugAll) && console.debug("ReconnectingWebSocket", "connection-timeout", g.url), j = !0, d.close(), j = !1
|
|
}, g.timeoutInterval);
|
|
h.onopen = function () {
|
|
clearTimeout(e), (g.debug || a.debugAll) && console.debug("ReconnectingWebSocket", "onopen", g.url), g.protocol = h.protocol, g.readyState = WebSocket.OPEN, g.reconnectAttempts = 0;
|
|
var d = l("open");
|
|
d.isReconnect = b, b = !1, k.dispatchEvent(d)
|
|
}, h.onclose = function (c) {
|
|
if (clearTimeout(e), h = null, i) g.readyState = WebSocket.CLOSED, k.dispatchEvent(l("close")); else {
|
|
g.readyState = WebSocket.CONNECTING;
|
|
var d = l("connecting");
|
|
d.code = c.code, d.reason = c.reason, d.wasClean = c.wasClean, k.dispatchEvent(d), b || j || ((g.debug || a.debugAll) && console.debug("ReconnectingWebSocket", "onclose", g.url), k.dispatchEvent(l("close")));
|
|
var e = g.reconnectInterval * Math.pow(g.reconnectDecay, g.reconnectAttempts);
|
|
setTimeout(function () {
|
|
g.reconnectAttempts++, g.open(!0)
|
|
}, e > g.maxReconnectInterval ? g.maxReconnectInterval : e)
|
|
}
|
|
}, h.onmessage = function (b) {
|
|
(g.debug || a.debugAll) && console.debug("ReconnectingWebSocket", "onmessage", g.url, b.data);
|
|
var c = l("message");
|
|
c.data = b.data, k.dispatchEvent(c)
|
|
}, h.onerror = function (b) {
|
|
(g.debug || a.debugAll) && console.debug("ReconnectingWebSocket", "onerror", g.url, b), k.dispatchEvent(l("error"))
|
|
}
|
|
}, 1 == this.automaticOpen && this.open(!1), this.send = function (b) {
|
|
if (h) return (g.debug || a.debugAll) && console.debug("ReconnectingWebSocket", "send", g.url, b), h.send(b);
|
|
throw"INVALID_STATE_ERR : Pausing to reconnect websocket"
|
|
}, this.close = function (a, b) {
|
|
"undefined" == typeof a && (a = 1e3), i = !0, h && h.close(a, b)
|
|
}, this.refresh = function () {
|
|
h && h.close()
|
|
}
|
|
}
|
|
|
|
return a.prototype.onopen = function () {
|
|
}, a.prototype.onclose = function () {
|
|
}, a.prototype.onconnecting = function () {
|
|
}, a.prototype.onmessage = function () {
|
|
}, a.prototype.onerror = function () {
|
|
}, a.debugAll = !1, a.CONNECTING = WebSocket.CONNECTING, a.OPEN = WebSocket.OPEN, a.CLOSING = WebSocket.CLOSING, a.CLOSED = WebSocket.CLOSED, a
|
|
});
|
|
</script>
|
|
<style>
|
|
.even\:bg-gray-50:nth-child(even) {
|
|
background-color: #f7fafc;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app" class="">
|
|
<div class="relative bg-indigo-600" style="marign-left: -1px">
|
|
<div class="max-w-screen-xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
|
|
<div class="pr-16 sm:text-center sm:px-16">
|
|
<p class="font-medium text-white flex justify-center">
|
|
<span class="inline-block">Waiting for requests on: <a class="underline" target="_blank"
|
|
href="%subdomains%">%subdomains%</a></span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-5 flex flex-row">
|
|
<div class="w-1/3 flex flex-col mr-5">
|
|
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
|
<span class="inline-flex rounded-md shadow-sm mb-4">
|
|
<button @click.prevent="clearLogs"
|
|
type="button"
|
|
class="inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs leading-4 font-medium rounded text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50 transition ease-in-out duration-150">
|
|
Clear
|
|
</button>
|
|
</span>
|
|
<div
|
|
class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200">
|
|
<table class="min-w-full">
|
|
<thead>
|
|
<tr>
|
|
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
|
URL
|
|
</th>
|
|
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
|
Response
|
|
</th>
|
|
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
|
Duration
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white">
|
|
<tr v-for="log in logs"
|
|
:class="{'bg-gray-100': currentLog === log}"
|
|
@click="setLog(log)">
|
|
<td class="cursor.pointer px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 font-medium text-gray-900">
|
|
<p>
|
|
{{ log.request.method }}
|
|
{{ log.request.uri }}
|
|
</p>
|
|
<span class="text-xs">{{ log.subdomain }}</span>
|
|
</td>
|
|
<td class="cursor.pointer px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
|
<div v-if="log.response">
|
|
{{ log.response.status }} - {{ log.response.reason }}
|
|
</div>
|
|
<div v-else>
|
|
...
|
|
</div>
|
|
</td>
|
|
<td class="cursor.pointer px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
|
<div v-if="log.response">
|
|
{{ log.duration }}ms
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="w-2/3 ml-5">
|
|
<div v-if="currentLog" class="bg-white shadow overflow-hidden sm:rounded-lg">
|
|
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 flex">
|
|
{{ currentLog.request.method }} {{ currentLog.request.uri }}
|
|
<div class="flex-grow"></div>
|
|
<span class="inline-flex rounded-md shadow-sm">
|
|
<button @click.prevent="replay(currentLog)"
|
|
type="button"
|
|
class="inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs leading-4 font-medium rounded text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50 transition ease-in-out duration-150">
|
|
Replay
|
|
</button>
|
|
</span>
|
|
<span class="inline-flex rounded-md shadow-sm ml-4">
|
|
<button
|
|
:data-clipboard-text="currentLog.request.curl"
|
|
type="button"
|
|
class="clipboard inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs leading-4 font-medium rounded text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50 transition ease-in-out duration-150">
|
|
Copy as curl
|
|
</button>
|
|
</span>
|
|
</h3>
|
|
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
|
|
Status code: {{ currentLog.response?.status}}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<div class="hidden sm:block">
|
|
<div class="border-b border-gray-200">
|
|
<nav class="-mb-px flex">
|
|
<a href="#"
|
|
@click.prevent="setView('request')"
|
|
:class="{
|
|
'border-indigo-500 text-indigo-600 text-indigo-600 focus:text-indigo-800 focus:border-indigo-700': view === 'request',
|
|
'border-border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300': view === 'response',
|
|
}"
|
|
class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm leading-5 focus:outline-none">
|
|
Request
|
|
</a>
|
|
<a href="#"
|
|
@click.prevent="setView('response')"
|
|
:class="{
|
|
'border-indigo-500 text-indigo-600': view === 'response',
|
|
'border-border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300': view === 'request',
|
|
}"
|
|
class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm leading-5 focus:outline-none">
|
|
Response
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="view === 'request'">
|
|
|
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-900">
|
|
Query Parameters
|
|
</dt>
|
|
</div>
|
|
<div v-for="(value, name) in currentLog.request.query"
|
|
:key="'query_' + name"
|
|
class="even:bg-gray-50 odd:bg-gray-50 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
|
{{ name }}
|
|
</dt>
|
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
|
{{ value }}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-900">
|
|
Post Parameters
|
|
</dt>
|
|
</div>
|
|
<div v-for="(parameter, name) in currentLog.request.post"
|
|
:key="'post_' + name"
|
|
class="even:bg-gray-50 odd:bg-gray-50 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
|
{{ parameter.name }}
|
|
</dt>
|
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
|
<span
|
|
v-if="parameter.is_file">File: {{ parameter.filename }} ({{ parameter.mime_type }})</span>
|
|
<span v-else>{{ parameter.value }}</span>
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-900">
|
|
Headers
|
|
</dt>
|
|
</div>
|
|
<div v-for="(value, header) in currentLog.request.headers"
|
|
:key="header"
|
|
class="even:bg-gray-50 odd:bg-gray-50 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
|
{{ header }}
|
|
</dt>
|
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
|
{{ value }}
|
|
</dd>
|
|
</div>
|
|
|
|
<div>
|
|
<pre class="p-6 prettyprint">{{ currentLog.request.body }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="view === 'response'">
|
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-900">
|
|
Headers
|
|
</dt>
|
|
</div>
|
|
<div v-for="(value, header) in currentLog.response.headers"
|
|
:key="header"
|
|
class="even:bg-gray-50 odd:bg-gray-50 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
|
{{ header }}
|
|
</dt>
|
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
|
{{ value }}
|
|
</dd>
|
|
</div>
|
|
<div v-if="currentLog.request.additional_data.length !== 0">
|
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-900">
|
|
Debug
|
|
</dt>
|
|
</div>
|
|
<div v-for="(value, key) in currentLog.request.additional_data"
|
|
:key="'debug'+key"
|
|
class="even:bg-gray-50 odd:bg-gray-50 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
|
{{ key }}
|
|
</dt>
|
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
|
<div v-html="value">
|
|
</dd>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt class="text-sm leading-5 font-medium text-gray-900">
|
|
Response
|
|
</dt>
|
|
<div>
|
|
<div class="sm:hidden">
|
|
<select class="form-select block w-full">
|
|
<option>My Account</option>
|
|
<option>Company</option>
|
|
<option selected>Team Members</option>
|
|
<option>Billing</option>
|
|
</select>
|
|
</div>
|
|
<div class="hidden sm:block">
|
|
<nav class="flex">
|
|
<a href="#"
|
|
@click.prevent="setActiveTab('raw')"
|
|
:class="{'outline-none text-gray-700 bg-gray-100': activeTab === 'raw'}"
|
|
class="px-3 py-2 font-medium text-sm leading-5 rounded-md text-gray-500 hover:text-gray-700">
|
|
Raw
|
|
</a>
|
|
<a href="#"
|
|
@click.prevent="setActiveTab('preview')"
|
|
:class="{'outline-none text-gray-700 bg-gray-100': activeTab === 'preview'}"
|
|
class="ml-4 px-3 py-2 font-medium text-sm leading-5 rounded-md text-gray-500 hover:text-gray-700 focus:outline-none focus:text-gray-700 focus:bg-gray-100">
|
|
Preview
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="activeTab === 'raw'">
|
|
<pre class="p-6 text-sm">{{ currentLog.response.body }}</pre>
|
|
</div>
|
|
<div v-if="activeTab === 'preview'">
|
|
<iframe :srcdoc="currentLog.response.body" style="height: 500px;" class="w-full h-full"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
|
|
data: {
|
|
currentLog: null,
|
|
view: 'request',
|
|
activeTab: 'raw',
|
|
logs: [],
|
|
},
|
|
|
|
methods: {
|
|
setActiveTab: function(tab) {
|
|
this.activeTab = tab;
|
|
},
|
|
clearLogs: function() {
|
|
fetch('/logs/clear');
|
|
},
|
|
setLog: function (log) {
|
|
this.currentLog = log;
|
|
|
|
this.$nextTick(function () {
|
|
let clipboard = new ClipboardJS('.clipboard');
|
|
});
|
|
},
|
|
setView: function (view) {
|
|
this.view = view;
|
|
},
|
|
replay: function (log) {
|
|
fetch('/replay/' + log.id);
|
|
},
|
|
connect: function () {
|
|
let conn = new ReconnectingWebSocket(`ws://${window.location.hostname}:${window.location.port}/socket`);
|
|
|
|
conn.onmessage = (e) => {
|
|
this.logs = JSON.parse(e.data);
|
|
};
|
|
},
|
|
loadLogs: function (log) {
|
|
fetch('/logs')
|
|
.then((response) => {
|
|
return response.json();
|
|
})
|
|
.then((data) => {
|
|
this.logs = data;
|
|
});
|
|
},
|
|
},
|
|
|
|
mounted: function () {
|
|
this.connect();
|
|
|
|
this.loadLogs();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|