mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-22 00:08:29 +02:00
fixes, meaningful error messages and new features
This commit is contained in:
1
templates/codemirror
Submodule
1
templates/codemirror
Submodule
Submodule templates/codemirror added at 0c8456c3bc
32
templates/config_editor/index.html
Normal file
32
templates/config_editor/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="/codemirror/lib/codemirror.css" rel="stylesheet" />
|
||||
<link href="/codemirror/theme/dracula.css" rel="stylesheet" />
|
||||
<link href="style.css" rel="stylesheet" />
|
||||
<title>Config Editor</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="file-navigation">
|
||||
<h3 class="navigation-header">Config Files</h3>
|
||||
<ul id="file-list">
|
||||
{{- range $_, $cfgFile := .}}
|
||||
<li id="file-{{$cfgFile}}">
|
||||
<a class="unselectable">{{$cfgFile}}</a>
|
||||
</li>
|
||||
{{- end}}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="config-editor"></div>
|
||||
</div>
|
||||
|
||||
<script src="/codemirror/lib/codemirror.js"></script>
|
||||
<script src="/codemirror/mode/yaml/yaml.js"></script>
|
||||
<script src="/codemirror/keymap/sublime.js"></script>
|
||||
<script src="/codemirror/addon/comment/comment.js"></script>
|
||||
<script src="index.js" onload="onLoad()"></script>
|
||||
</body>
|
||||
</html>
|
||||
75
templates/config_editor/index.js
Normal file
75
templates/config_editor/index.js
Normal file
@@ -0,0 +1,75 @@
|
||||
let currentFile = "config.yml";
|
||||
let editorElement = document.getElementById("config-editor");
|
||||
let fileListElement = document.getElementById("file-list");
|
||||
let editor = CodeMirror(editorElement, {
|
||||
lineNumbers: true,
|
||||
mode: "yaml",
|
||||
theme: "dracula",
|
||||
autofocus: true,
|
||||
lineWiseCopyCut: true,
|
||||
keyMap: "sublime",
|
||||
tabSize: 2
|
||||
});
|
||||
|
||||
function loadFile(fileName) {
|
||||
if (fileName === undefined) {
|
||||
return;
|
||||
}
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("GET", `/config/${fileName}`, true);
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200) {
|
||||
let old_nav_item = document.getElementById(`file-${currentFile}`);
|
||||
old_nav_item.classList.remove("active");
|
||||
editor.setValue(req.responseText);
|
||||
currentFile = fileName;
|
||||
let new_nav_item = document.getElementById(`file-${currentFile}`);
|
||||
new_nav_item.classList.add("active");
|
||||
document.title = `${currentFile} - Config Editor`;
|
||||
console.log(`loaded ${currentFile}`);
|
||||
} else {
|
||||
let msg = `Failed to load ${fileName}: ` + req.responseText;
|
||||
alert(msg);
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
}
|
||||
|
||||
function saveFile(filename, content) {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("PUT", `/config/${filename}`, true);
|
||||
req.setRequestHeader("Content-Type", "text/plain");
|
||||
req.send(content);
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200) {
|
||||
alert("Saved " + filename);
|
||||
} else {
|
||||
alert("Error: " + req.responseText);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
editor.setSize("100wh", "100vh");
|
||||
editor.setOption("extraKeys", {
|
||||
Tab: function (cm) {
|
||||
const spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
|
||||
cm.replaceSelection(spaces);
|
||||
},
|
||||
"Ctrl-S": function (cm) {
|
||||
saveFile(currentFile, cm.getValue());
|
||||
},
|
||||
});
|
||||
fileListElement.addEventListener("click", function (e) {
|
||||
if (e.target === null) {
|
||||
return;
|
||||
}
|
||||
loadFile(e.target.text);
|
||||
});
|
||||
function onLoad() {
|
||||
loadFile(currentFile);
|
||||
}
|
||||
60
templates/config_editor/style.css
Normal file
60
templates/config_editor/style.css
Normal file
@@ -0,0 +1,60 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: 14px !important;
|
||||
font-family: monospace !important;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
.navigation-header {
|
||||
color: #f8f8f2 !important;
|
||||
padding-left: 2em;
|
||||
display: block;
|
||||
}
|
||||
.file-navigation {
|
||||
width: 250px;
|
||||
height: auto;
|
||||
overflow-y: auto;
|
||||
background: #282a36 !important;
|
||||
}
|
||||
.file-navigation ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.file-navigation li {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.file-navigation a {
|
||||
color: #f8f8f2 !important;
|
||||
text-decoration: none;
|
||||
padding-left: 4em;
|
||||
padding-right: 4em;
|
||||
display: block;
|
||||
}
|
||||
.active {
|
||||
font-weight: bold;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.unselectable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.CodeMirror * {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
.CodeMirror pre {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
#config-editor {
|
||||
flex-grow: 1;
|
||||
}
|
||||
23
templates/index.html
Normal file
23
templates/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="style.css" rel="stylesheet" />
|
||||
<title>go-proxy</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.js"></script>
|
||||
<div id="sidenav" class="sidenav">
|
||||
<a href="javascript:void(0)" class="closebtn" onclick="closeNav()"
|
||||
>×</a
|
||||
>
|
||||
<a href="#" onClick='setContent("/panel")'>Panel</a>
|
||||
<a href="#" onClick='setContent("/config_editor")'>Config Editor</a>
|
||||
</div>
|
||||
<a class="openbtn" id="openbtn" onclick="openNav()">≡</a>
|
||||
<div id="main">
|
||||
<iframe id="content" src="/config_editor" title="panel"></iframe>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
27
templates/main.js
Normal file
27
templates/main.js
Normal file
@@ -0,0 +1,27 @@
|
||||
function contentIFrame() {
|
||||
return document.getElementById("content");
|
||||
}
|
||||
|
||||
function openNavBtn() {
|
||||
return document.getElementById("openbtn");
|
||||
}
|
||||
|
||||
function sideNav() {
|
||||
return document.getElementById("sidenav");
|
||||
}
|
||||
|
||||
function setContent(path) {
|
||||
contentIFrame().attributes.src.value = path;
|
||||
}
|
||||
|
||||
function openNav() {
|
||||
sideNav().style.width = "250px";
|
||||
contentIFrame().style.marginLeft = "250px";
|
||||
openNavBtn().style.display = "none";
|
||||
}
|
||||
|
||||
function closeNav() {
|
||||
sideNav().style.width = "0";
|
||||
contentIFrame().style.marginLeft = "0px";
|
||||
openNavBtn().style.display = "inline-block";
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #131516;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
table th:first-child {
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
table th:last-child {
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
table td:first-of-type {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
table td:last-of-type {
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
table caption {
|
||||
color: antiquewhite;
|
||||
}
|
||||
|
||||
.health-circle {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
background-color: #28a745;
|
||||
border-radius: 50%;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
<title>Route Panel</title>
|
||||
<script>
|
||||
function checkHealth(url, cell) {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState != 4) {
|
||||
return
|
||||
}
|
||||
if (this.status === 200) {
|
||||
cell.innerHTML = '<div class="health-circle"></div>'; // Green circle for healthy
|
||||
} else {
|
||||
cell.innerHTML = '<div class="health-circle" style="background-color: #dc3545;"></div>'; // Red circle for unhealthy
|
||||
}
|
||||
};
|
||||
url = window.location.origin + '/checkhealth?target=' + encodeURIComponent(url);
|
||||
xhttp.open("HEAD", url, true);
|
||||
xhttp.send();
|
||||
}
|
||||
|
||||
function updateHealthStatus() {
|
||||
let rows = document.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
let url = row.querySelector('#url-cell').textContent;
|
||||
let cell = row.querySelector('#health-cell'); // Health column cell
|
||||
checkHealth(url, cell);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
updateHealthStatus();
|
||||
|
||||
// Update health status every 5 seconds
|
||||
setInterval(updateHealthStatus, 5000);
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="m-3">
|
||||
<div class="container">
|
||||
<h1 class="text-success">
|
||||
Route Panel
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div class="table-responsive col-md-6">
|
||||
<table class="table table-striped table-dark caption-top w-auto">
|
||||
<caption>HTTP Proxies</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias</th>
|
||||
<th>Path</th>
|
||||
<th>Path Mode</th>
|
||||
<th>URL</th>
|
||||
<th>Health</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $alias, $pathPoolMap := .HTTPRoutes.Iterator}}
|
||||
{{range $path, $lbPool := $pathPoolMap.Iterator}}
|
||||
{{range $_, $route := $lbPool.Iterator}}
|
||||
<tr>
|
||||
<td>{{$alias}}</td>
|
||||
<td>{{$path}}</td>
|
||||
<td>{{$route.PathMode}}</td>
|
||||
<td id="url-cell">{{$route.Url.String}}</td>
|
||||
<td class="align-middle" id="health-cell">
|
||||
<div class="health-circle"></div>
|
||||
</td> <!-- Health column -->
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-responsive col-md-6">
|
||||
<table class="table table-striped table-dark caption-top w-auto">
|
||||
<caption>Streams</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias</th>
|
||||
<th>Source</th>
|
||||
<th>Target</th>
|
||||
<th>Health</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $_, $route := .StreamRoutes.Iterator}}
|
||||
<tr>
|
||||
<td>{{$route.Alias}}</td>
|
||||
<td>{{$route.ListeningUrl}}</td>
|
||||
<td id="url-cell">{{$route.TargetUrl}}</td>
|
||||
<td class="align-middle" id="health-cell">
|
||||
<div class="health-circle"></div>
|
||||
</td> <!-- Health column -->
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
6
templates/panel/bootstrap.min.css
vendored
Normal file
6
templates/panel/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
79
templates/panel/index.html
Executable file
79
templates/panel/index.html
Executable file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="style.css" rel="stylesheet" />
|
||||
<title>Route Panel</title>
|
||||
</head>
|
||||
|
||||
<body class="m-3">
|
||||
<script src="index.js" defer></script>
|
||||
<div class="container">
|
||||
<h1 class="text-success">Route Panel</h1>
|
||||
<div class="row">
|
||||
<div class="table-responsive col-md-auto flex-shrink-1">
|
||||
<table class="table table-striped table-dark caption-top">
|
||||
<caption>
|
||||
HTTP Proxies
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias</th>
|
||||
<th>Path</th>
|
||||
<th>Path Mode</th>
|
||||
<th>URL</th>
|
||||
<th>Health</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $alias, $pathPoolMap := .HTTPRoutes.Iterator}} {{range
|
||||
$path, $lbPool := $pathPoolMap.Iterator}} {{range $_, $route :=
|
||||
$lbPool.Iterator}}
|
||||
<tr>
|
||||
<td>{{$alias}}</td>
|
||||
<td>{{$path}}</td>
|
||||
<td>{{$route.PathMode}}</td>
|
||||
<td id="url-cell">{{$route.Url.String}}</td>
|
||||
<td class="align-middle" id="health-cell">
|
||||
<div class="health-circle"></div>
|
||||
</td>
|
||||
<!-- Health column -->
|
||||
</tr>
|
||||
{{end}} {{end}} {{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-responsive col-md">
|
||||
<table class="table table-striped table-dark caption-top w-auto">
|
||||
<caption>
|
||||
Streams
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias</th>
|
||||
<th>Source</th>
|
||||
<th>Target</th>
|
||||
<th>Health</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $_, $route := .StreamRoutes.Iterator}}
|
||||
<tr>
|
||||
<td>{{$route.Alias}}</td>
|
||||
<td>{{$route.ListeningUrl}}</td>
|
||||
<td id="url-cell">{{$route.TargetUrl}}</td>
|
||||
<td class="align-middle" id="health-cell">
|
||||
<div class="health-circle"></div>
|
||||
</td>
|
||||
<!-- Health column -->
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
34
templates/panel/index.js
Normal file
34
templates/panel/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
function checkHealth(url, cell) {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState != 4) {
|
||||
return;
|
||||
}
|
||||
if (this.status === 200) {
|
||||
cell.innerHTML = '<div class="health-circle"></div>'; // Green circle for healthy
|
||||
} else {
|
||||
cell.innerHTML =
|
||||
'<div class="health-circle" style="background-color: #dc3545;"></div>'; // Red circle for unhealthy
|
||||
}
|
||||
};
|
||||
url =
|
||||
window.location.origin + "/checkhealth?target=" + encodeURIComponent(url);
|
||||
xhttp.open("HEAD", url, true);
|
||||
xhttp.send();
|
||||
}
|
||||
|
||||
function updateHealthStatus() {
|
||||
let rows = document.querySelectorAll("tbody tr");
|
||||
rows.forEach((row) => {
|
||||
let url = row.querySelector("#url-cell").textContent;
|
||||
let cell = row.querySelector("#health-cell"); // Health column cell
|
||||
checkHealth(url, cell);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
updateHealthStatus();
|
||||
|
||||
// Update health status every 5 seconds
|
||||
setInterval(updateHealthStatus, 5000);
|
||||
});
|
||||
43
templates/panel/style.css
Normal file
43
templates/panel/style.css
Normal file
@@ -0,0 +1,43 @@
|
||||
body {
|
||||
background-color: #131516;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
table th:first-child {
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
table th:last-child {
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
table td:first-of-type {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
table td:last-of-type {
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
table caption {
|
||||
color: antiquewhite;
|
||||
}
|
||||
|
||||
.health-circle {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
background-color: #28a745;
|
||||
border-radius: 50%;
|
||||
margin: auto;
|
||||
}
|
||||
68
templates/style.css
Normal file
68
templates/style.css
Normal file
@@ -0,0 +1,68 @@
|
||||
html,
|
||||
body {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #111;
|
||||
overflow-x: hidden;
|
||||
padding-top: 32px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.sidenav a {
|
||||
padding: 8px 8px 8px 24px;
|
||||
text-decoration: none;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #818181;
|
||||
display: block;
|
||||
}
|
||||
.sidenav a:hover {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.sidenav .closebtn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 24px;
|
||||
font-size: 24px;
|
||||
margin-left: 42px;
|
||||
}
|
||||
|
||||
.openbtn {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 16;
|
||||
left: 16;
|
||||
font: 24px bold monospace;
|
||||
color: #f8f8f2 !important;
|
||||
}
|
||||
|
||||
#main {
|
||||
transition: margin-left 0.3s;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#content {
|
||||
transition: margin-left 0.3s;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
margin: 0;
|
||||
margin-left: 0px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
Reference in New Issue
Block a user