Replace old static folder with new static folder

This commit is contained in:
TwinProduction 2021-01-28 23:25:29 -05:00
parent fbb5d48bf7
commit 601d676e34
21 changed files with 38 additions and 462 deletions

View File

@ -3,3 +3,4 @@ Dockerfile
.github .github
.idea .idea
.git .git
web/app

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus
FROM scratch FROM scratch
COPY --from=builder /app/gatus . COPY --from=builder /app/gatus .
COPY --from=builder /app/config.yaml ./config/config.yaml COPY --from=builder /app/config.yaml ./config/config.yaml
COPY --from=builder /app/static static/ COPY --from=builder /app/web/static ./app/web/static/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ENV PORT=8080 ENV PORT=8080
EXPOSE ${PORT} EXPOSE ${PORT}

View File

@ -1,4 +1,4 @@
![Gatus](static/logo-with-name.png) ![Gatus](assets/logo-with-name.png)
![build](https://github.com/TwinProduction/gatus/workflows/build/badge.svg?branch=master) ![build](https://github.com/TwinProduction/gatus/workflows/build/badge.svg?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/TwinProduction/gatus?)](https://goreportcard.com/report/github.com/TwinProduction/gatus) [![Go Report Card](https://goreportcard.com/badge/github.com/TwinProduction/gatus?)](https://goreportcard.com/report/github.com/TwinProduction/gatus)

View File

@ -1,14 +1,14 @@
services: services:
- name: frontend - name: Front End
group: core group: Excuse Me
url: "https://twinnation.org/health" url: "https://twinnation.org/health"
interval: 1m interval: 30s
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
- "[BODY].status == UP" - "[BODY].status == UP"
- "[RESPONSE_TIME] < 70" - "[RESPONSE_TIME] < 23"
- name: backend - name: back-end
group: core group: core
url: "http://example.org/" url: "http://example.org/"
interval: 5m interval: 5m
@ -29,7 +29,7 @@ services:
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
- name: cat-fact - name: cat fact
url: "https://cat-fact.herokuapp.com/facts/random" url: "https://cat-fact.herokuapp.com/facts/random"
interval: 5m interval: 5m
conditions: conditions:

View File

@ -55,11 +55,12 @@ func Handle() {
func CreateRouter(cfg *config.Config) *mux.Router { func CreateRouter(cfg *config.Config) *mux.Router {
router := mux.NewRouter() router := mux.NewRouter()
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses"), secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET") router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses"), secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET")
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses/{key}"), secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET") router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses/{key}"), secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/badges/uptime/{duration}/{identifier}"), badgeHandler).Methods("GET") router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/badges/uptime/{duration}/{identifier}"), badgeHandler).Methods("GET")
router.HandleFunc(cfg.Web.PrependWithContextRoot("/health"), healthHandler).Methods("GET") router.HandleFunc(cfg.Web.PrependWithContextRoot("/health"), healthHandler).Methods("GET")
router.PathPrefix(cfg.Web.ContextRoot).Handler(GzipHandler(http.StripPrefix(cfg.Web.ContextRoot, http.FileServer(http.Dir("./static"))))) router.PathPrefix(cfg.Web.ContextRoot).Handler(GzipHandler(http.StripPrefix(cfg.Web.ContextRoot, http.FileServer(http.Dir("./web/static")))))
if cfg.Metrics { if cfg.Metrics {
router.Handle(cfg.Web.PrependWithContextRoot("/metrics"), promhttp.Handler()).Methods("GET") router.Handle(cfg.Web.PrependWithContextRoot("/metrics"), promhttp.Handler()).Methods("GET")
} }
@ -151,5 +152,10 @@ func healthHandler(writer http.ResponseWriter, _ *http.Request) {
// favIconHandler handles requests for /favicon.ico // favIconHandler handles requests for /favicon.ico
func favIconHandler(writer http.ResponseWriter, request *http.Request) { func favIconHandler(writer http.ResponseWriter, request *http.Request) {
http.ServeFile(writer, request, "./static/favicon.ico") http.ServeFile(writer, request, "./web/static/favicon.ico")
}
// spaHandler handles requests for /favicon.ico
func spaHandler(writer http.ResponseWriter, request *http.Request) {
http.ServeFile(writer, request, "./web/static/index.html")
} }

File diff suppressed because one or more lines are too long

View File

@ -1,438 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Health Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="./tailwind.min.css" rel="stylesheet" />
<style>
html, body {
background-color: #f7f9fb;
}
html {
height: 100%;
}
#global {
max-width: 1140px;
}
#results div.container:first-child {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
#results div.container:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
border-bottom-width: 1px;
border-color: #dee2e6;
border-style: solid;
}
#results .service-group-content > div:nth-child(1) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.status {
cursor: pointer;
transition: all 500ms ease-in-out;
overflow-x: hidden;
color: white;
width: 5%;
font-size: 75%;
font-weight: 700;
text-align: center;
}
.status:hover {
opacity: 0.7;
transition: opacity 100ms ease-in-out;
color: black;
}
.status-over-time {
overflow: auto;
}
.status-over-time>span:not(:last-child) {
margin-left: 2px;
}
.status-time-ago {
color: #6a737d;
opacity: 0.5;
margin-top: 5px;
}
.status-min-max-ms {
overflow-x: hidden;
}
.title {
font-size: 2.5rem;
}
#tooltip {
position: fixed;
top: 0;
left: 0;
background-color: white;
border: 1px solid lightgray;
border-radius: 4px;
padding: 6px;
font-size: 13px;
}
#tooltip code {
color: #212529;
line-height: 1;
}
#tooltip .tooltip-title {
font-weight: bold;
margin-bottom: 0;
display: block;
}
#tooltip .tooltip-title {
margin-top: 8px;
}
#tooltip>.tooltip-title:first-child {
margin-top: 0;
}
#social {
position: fixed;
right: 5px;
bottom: 5px;
padding: 5px;
margin: 0;
z-index: 100;
}
#social img {
opacity: 0.3;
}
#social img:hover {
opacity: 1;
}
#settings {
position: fixed;
left: 5px;
bottom: 5px;
padding: 5px;
}
#settings select:focus {
box-shadow: none;
}
.service-group {
cursor: pointer;
user-select: none;
}
.service-group h5:hover {
color: #1b1e21 !important;
}
.bg-success {
background-color: #28a745;
}
.text-monospace {
font-family: Consolas, monospace;
}
</style>
</head>
<body>
<div class="container mx-auto rounded shadow-xl border my-3 p-5" id="global">
<div class="mb-2">
<div class="flex flex-wrap">
<div class="w-2/3 text-left my-auto">
<div class="title font-light">Health Status</div>
</div>
<div class="w-1/3 flex justify-end">
<img src="logo.png" alt="Gatus" style="min-width: 50px; max-width: 200px; width: 20%;"/>
</div>
</div>
</div>
<div id="results"></div>
</div>
<div id="tooltip" style="display: none">
<div class="tooltip-title">Timestamp:</div>
<code id="tooltip-timestamp">...</code>
<div class="tooltip-title">Response time:</div>
<code id="tooltip-response-time">...</code>
<div class="tooltip-title">Conditions:</div>
<code id="tooltip-conditions">...</code>
<div id="tooltip-errors-container">
<div class="tooltip-title">Errors:</div>
<code id="tooltip-errors">...</code>
</div>
</div>
<script src="./jquery.min.js"></script>
<div id="social">
<a href="https://github.com/TwinProduction/gatus" target="_blank" title="Gatus on GitHub">
<img src="./github.png" alt="GitHub" width="32" height="auto" />
</a>
</div>
<div id="settings">
<div class="flex bg-gray-200 rounded border border-gray-300 shadow">
<div class="rounded-xl py-1 px-2 text-gray-600 text-sm">
&#x21bb;
</div>
<select class="text-center text-gray-500 text-sm" id="refresh-rate">
<option value="10">10s</option>
<option value="30" selected>30s</option>
<option value="60">1m</option>
<option value="120">2m</option>
<option value="300">5m</option>
<option value="600">10m</option>
</select>
</div>
</div>
<script>
let maximumNumberOfResults = 20;
let serviceStatuses = {};
let timerHandler = 0;
let refreshIntervalHandler = 0;
let userClickedStatus = false;
// TODO: make this variable configurable and persist the choice in localStorage
let showStatusOnHover = true;
function showTooltip(serviceName, index, element) {
//userClickedStatus = false;
clearTimeout(timerHandler);
let serviceResult = serviceStatuses[serviceName].results[index];
$("#tooltip-timestamp").text(prettifyTimestamp(serviceResult.timestamp));
$("#tooltip-response-time").text(parseInt(serviceResult.duration/1000000) + "ms");
// Populate the condition section
let conditions = "";
for (let i in serviceResult.conditionResults) {
let conditionResult = serviceResult.conditionResults[i];
conditions += (conditionResult.success ? "&#10003;" : "X") + " ~ " + htmlEntities(conditionResult.condition) + "<br />";
}
$("#tooltip-conditions").html(conditions);
// Populate the error section only if there are errors
if (serviceResult.errors && serviceResult.errors.length > 0) {
let errors = "";
for (let i in serviceResult.errors) {
errors += "- " + htmlEntities(serviceResult.errors[i]) + "<br />";
}
$("#tooltip-errors").html(errors);
$("#tooltip-errors-container").show();
} else {
$("#tooltip-errors-container").hide();
}
// Position tooltip
$("#tooltip").css({top: "0", left: "0"}).show();
let targetTopPosition = element.getBoundingClientRect().y + 30;
let targetLeftPosition = element.getBoundingClientRect().x;
// Make adjustments if necessary
let tooltipBoundingClientRect = document.querySelector('#tooltip').getBoundingClientRect();
if (targetLeftPosition + window.scrollX + tooltipBoundingClientRect.width + 50 > document.body.getBoundingClientRect().width) {
targetLeftPosition = element.getBoundingClientRect().x - tooltipBoundingClientRect.width + element.getBoundingClientRect().width;
if (targetLeftPosition < 0) {
targetLeftPosition += -targetLeftPosition;
}
}
if (targetTopPosition + window.scrollY + tooltipBoundingClientRect.height + 50 > document.body.getBoundingClientRect().height && targetTopPosition >= 0) {
targetTopPosition = element.getBoundingClientRect().y - (tooltipBoundingClientRect.height + 10);
if (targetTopPosition < 0) {
targetTopPosition = element.getBoundingClientRect().y + 30;
}
}
$("#tooltip").css({top: targetTopPosition + "px", left: targetLeftPosition + "px"});
}
function fadeTooltip() {
if (!userClickedStatus) {
timerHandler = setTimeout(function () {
$("#tooltip").hide();
}, 50);
}
}
function toggleTooltip(serviceName, index, element) {
console.log("userClickedStatus="+userClickedStatus);
if (!userClickedStatus) {
showTooltip(serviceName, index, element);
userClickedStatus = true;
} else {
$("#tooltip").hide();
userClickedStatus = false;
}
}
function createStatusBadge(serviceStatusIndex, index, success) {
if (success) {
if (showStatusOnHover) {
return "<span class='status rounded bg-success' onmouseenter='showTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)' onmouseleave='fadeTooltip()' onclick='userClickedStatus = !userClickedStatus;'>&#10003;</span>";
} else {
return "<span class='status rounded bg-success' onclick='toggleTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)'>&#10003;</span>";
}
}
if (showStatusOnHover) {
return "<span class='status rounded bg-red-600' onmouseenter='showTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)' onmouseleave='fadeTooltip()' onclick='userClickedStatus = !userClickedStatus;'>X</span>";
} else {
return "<span class='status rounded bg-red-600' onclick='toggleTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)'>X</span>";
}
}
function createBlankStatusBadge() {
return "<span class='status rounded border border-dashed'> </span>";
}
function refreshStatuses() {
$.getJSON("./api/v1/statuses", function (data) {
// Update the table only if there's a change
if (JSON.stringify(serviceStatuses) !== JSON.stringify(data)) {
serviceStatuses = data;
buildTable();
}
});
}
function buildTable() {
let outputByGroup = {};
for (let serviceStatusIndex in serviceStatuses) {
let serviceStatusOverTime = "";
let serviceStatus = serviceStatuses[serviceStatusIndex];
let hostname = serviceStatus.results[serviceStatus.results.length-1].hostname;
let minResponseTime = null;
let maxResponseTime = null;
let newestTimestamp = null;
let oldestTimestamp = null;
for (let resultIndex in serviceStatus.results) {
let serviceResult = serviceStatus.results[resultIndex];
serviceStatusOverTime = createStatusBadge(serviceStatusIndex, resultIndex, serviceResult.success) + serviceStatusOverTime;
const responseTime = parseInt(serviceResult.duration/1000000);
if (minResponseTime == null || minResponseTime > responseTime) {
minResponseTime = responseTime;
}
if (maxResponseTime == null || maxResponseTime < responseTime) {
maxResponseTime = responseTime;
}
const timestamp = new Date(serviceResult.timestamp);
if (newestTimestamp == null || newestTimestamp < timestamp) {
newestTimestamp = timestamp;
}
if (oldestTimestamp == null || oldestTimestamp > timestamp) {
oldestTimestamp = timestamp;
}
}
for (let i = serviceStatus.results.length; i < maximumNumberOfResults; i++) {
serviceStatusOverTime += createBlankStatusBadge();
}
let output = ""
+ "<div class='container px-3 py-3 border-l border-r border-t rounded-none'>"
+ " <div class='flex flex-wrap mb-2'>"
+ " <div class='w-3/4'>"
+ " <span class='font-bold'>" + serviceStatus.name + "</span> <span class='text-gray-500 font-light'>- " + hostname + "</span>"
+ " </div>"
+ " <div class='w-1/4 text-right'>"
+ " <span class='font-light status-min-max-ms'>" + (minResponseTime === maxResponseTime ? minResponseTime : (minResponseTime + "-" + maxResponseTime)) + "ms</span>"
+ " </div>"
+ " </div>"
+ " <div class=''>"
+ " <div class='status-over-time flex flex-row-reverse'>"
+ " " + serviceStatusOverTime
+ " </div>"
+ " </div>"
+ " <div class='flex flex-wrap status-time-ago'>"
+ " <div class='w-1/2'>"
+ " " + generatePrettyTimeAgo(oldestTimestamp)
+ " </div>"
+ " <div class='w-1/2 text-right'>"
+ " " + generatePrettyTimeAgo(newestTimestamp)
+ " </div>"
+ " </div>"
+ "</div>";
// create an empty entry if this group is new
if (!outputByGroup[serviceStatus.group]) {
outputByGroup[serviceStatus.group] = "";
}
outputByGroup[serviceStatus.group] += output;
}
let output = "";
for (let group in outputByGroup) {
// Services that don't have a group should be skipped and left for last
if (group === 'undefined') {
continue
}
let key = group.replace(/[^a-zA-Z0-9]/g, '');
let existingGroupContentSelector = $("#service-group-" + key + "-content");
let isCurrentlyHidden = existingGroupContentSelector.length && existingGroupContentSelector[0].style.display === 'none';
let groupStatus = "<span class='text-green-600'>&#10003;</span>";
if (outputByGroup[group].includes("bg-red-600")) {
groupStatus = "<span class='text-yellow-400'>~</span>";
}
output += ""
+ "<div class='" + (output.length ? 'mt-4' : 'mt-3') + "'>"
+ " <div class='container pt-2 border service-group' id='service-group-" + key + "' data-group='" + key + "' onclick='toggleGroup(this)'>"
+ " <h5 class='text-monospace text-gray-400 text-xl font-medium pb-2 px-3'>"
+ " " + groupStatus + " " + group
+ " <span class='float-right service-group-arrow' id='service-group-" + key + "-arrow'>" + (isCurrentlyHidden ? "&#9660;" : "&#9650;") + "</span>"
+ " </h5>"
+ " </div>"
+ " <div class='service-group-content' id='service-group-" + key + "-content' style='" + (isCurrentlyHidden ? "display: none;" : "") + "'>"
+ " " + outputByGroup[group]
+ " </div>"
+ "</div>";
}
// Add all services that don't have a group at the end
if (outputByGroup['undefined']) {
output += ""
+ "<div class='" + (output.length ? 'mt-4' : 'mt-3') + "'>"
+ " " + outputByGroup['undefined']
+ "</div>"
}
$("#results").html(output);
}
function toggleGroup(element) {
let selector = $("#service-group-" + element.dataset.group + "-content");
selector.toggle("fast", function() {
if (selector.length && selector[0].style.display === 'none') {
$("#service-group-" + element.dataset.group + "-arrow").html("&#9660;");
} else {
$("#service-group-" + element.dataset.group + "-arrow").html("&#9650;");
}
});
}
function prettifyTimestamp(timestamp) {
let date = new Date(timestamp);
let YYYY = date.getFullYear();
let MM = ((date.getMonth()+1)<10?"0":"")+""+(date.getMonth()+1);
let DD = ((date.getDate())<10?"0":"")+""+(date.getDate());
let hh = ((date.getHours())<10?"0":"")+""+(date.getHours());
let mm = ((date.getMinutes())<10?"0":"")+""+(date.getMinutes());
let ss = ((date.getSeconds())<10?"0":"")+""+(date.getSeconds());
return YYYY + "-" + MM + "-" + DD + " " + hh + ":" + mm + ":" + ss;
}
function generatePrettyTimeAgo(t) {
let differenceInMs = new Date().getTime() - new Date(t).getTime();
if (differenceInMs > 3600000) {
let hours = (differenceInMs/3600000).toFixed(0);
return hours + " hour" + (hours !== "1" ? "s" : "") + " ago";
}
if (differenceInMs > 60000) {
let minutes = (differenceInMs/60000).toFixed(0);
return minutes + " minute" + (minutes !== "1" ? "s" : "") + " ago";
}
return (differenceInMs/1000).toFixed(0) + " seconds ago";
}
function htmlEntities(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function setRefreshInterval(seconds) {
refreshStatuses();
refreshIntervalHandler = setInterval(function() {
refreshStatuses();
}, seconds * 1000);
}
$("#refresh-rate").change(function() {
clearInterval(refreshIntervalHandler);
setRefreshInterval($(this).val());
});
setRefreshInterval(30);
$("#refresh-rate").val(30);
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because one or more lines are too long

View File

@ -85,7 +85,7 @@ export default {
methods: { methods: {
fetchData() { fetchData() {
console.log("[Details][fetchData] Fetching data"); console.log("[Details][fetchData] Fetching data");
fetch(`${SERVER_URL}/api/v1/statuses/${this.$route.params.key}`) fetch(`${this.serverUrl}/api/v1/statuses/${this.$route.params.key}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) { if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
@ -96,8 +96,10 @@ export default {
if (i === data.events.length-1) { if (i === data.events.length-1) {
if (event.type === "UNHEALTHY") { if (event.type === "UNHEALTHY") {
event.fancyText = "Service is unhealthy"; event.fancyText = "Service is unhealthy";
} else { } else if (event.type === "HEALTHY") {
event.fancyText = "Service is healthy"; event.fancyText = "Service is healthy";
} else if (event.type === "START") {
event.fancyText = "Monitoring started";
} }
} else { } else {
let nextEvent = data.events[i+1]; let nextEvent = data.events[i+1];
@ -121,7 +123,7 @@ export default {
}); });
}, },
generateBadgeImageURL(duration) { generateBadgeImageURL(duration) {
return `${SERVER_URL}/api/v1/badges/uptime/${duration}/${this.serviceStatus.key}`; return `${this.serverUrl}/api/v1/badges/uptime/${duration}/${this.serviceStatus.key}`;
}, },
prettifyUptime(uptime) { prettifyUptime(uptime) {
if (!uptime) { if (!uptime) {
@ -140,7 +142,9 @@ export default {
data() { data() {
return { return {
serviceStatus: {}, serviceStatus: {},
events: [] events: [],
// Since this page isn't at the root, we need to modify the server URL a bit
serverUrl: SERVER_URL === '.' ? '..' : SERVER_URL,
} }
}, },
created() { created() {

View File

@ -1,4 +1,5 @@
module.exports = { module.exports = {
filenameHashing: false, filenameHashing: false,
productionSourceMap: false productionSourceMap: false,
outputDir: '../static'
} }

3
web/static/css/app.css Normal file

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

1
web/static/index.html Normal file
View File

@ -0,0 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>Health Dashboard | Gatus</title><link href="/css/app.css" rel="preload" as="style"><link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"><link href="/css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.js"></script><script src="/js/app.js"></script></body></html>

1
web/static/js/app.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long