mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
portal: Status indicator badge (#1703)
Did some research on status page providers to manage incidents. statuspage.io seems to be easy to use and cost-effective, fairly popular and provides a good amount of flexibility to customize emails, notifications, etc. Super easy to set up and use but am not married to it if anyone feels strongly about using another incident management service. https://firezone.statuspage.io ## Demo: <img width="235" alt="Screenshot 2023-06-27 at 8 07 29 AM" src="https://github.com/firezone/firezone/assets/167144/8ad12b9b-7345-4a5d-bf43-c8af798d85f9">
This commit is contained in:
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": false
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@fontsource/source-sans-pro";
|
||||
import "@fontsource/source-sans-pro"
|
||||
|
||||
// Import CSS generated by Tailwind compiler
|
||||
import "../tmp/tailwind/app.css";
|
||||
import "../tmp/tailwind/app.css"
|
||||
|
||||
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
||||
import "phoenix_html"
|
||||
@@ -13,15 +13,21 @@ import "flowbite/dist/flowbite.phoenix.js"
|
||||
import { Socket } from "phoenix"
|
||||
import { LiveSocket } from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
import Hooks from "./hooks"
|
||||
|
||||
// Read CSRF token from the meta tag and use it in the LiveSocket params
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })
|
||||
let csrfToken = document
|
||||
.querySelector("meta[name='csrf-token']")
|
||||
.getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, {
|
||||
hooks: Hooks,
|
||||
params: { _csrf_token: csrfToken },
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
|
||||
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
|
||||
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
|
||||
window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300))
|
||||
window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide())
|
||||
|
||||
// connect if there are any LiveViews on the page
|
||||
liveSocket.connect()
|
||||
|
||||
35
elixir/apps/web/assets/js/hooks.js
Normal file
35
elixir/apps/web/assets/js/hooks.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import StatusPage from "../vendor/status_page"
|
||||
|
||||
let Hooks = {}
|
||||
|
||||
// Update status indicator when sidebar is mounted or updated
|
||||
let statusIndicatorClassNames = {
|
||||
none: "bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300",
|
||||
minor:
|
||||
"bg-yellow-100 text-yellow-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-yellow-900 dark:text-yellow-300",
|
||||
major:
|
||||
"bg-orange-100 text-orange-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-orange-900 dark:text-orange-300",
|
||||
critical:
|
||||
"bg-red-100 text-red-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300",
|
||||
}
|
||||
const statusUpdater = function () {
|
||||
const self = this
|
||||
const sp = new StatusPage.page({ page: "firezone" })
|
||||
sp.summary({
|
||||
success: function (data) {
|
||||
self.el.innerHTML = `<span class="${
|
||||
statusIndicatorClassNames[data.status.indicator]
|
||||
}">${data.status.description}</span>`
|
||||
},
|
||||
error: function (data) {
|
||||
console.error("An error occured while fetching status page data")
|
||||
self.el.innerHTML = `<span class="${statusIndicatorClassNames.minor}">Unable to fetch status</span>`
|
||||
},
|
||||
})
|
||||
}
|
||||
Hooks.StatusPage = {
|
||||
mounted: statusUpdater,
|
||||
updated: statusUpdater,
|
||||
}
|
||||
|
||||
export default Hooks
|
||||
184
elixir/apps/web/assets/vendor/status_page.js
vendored
Normal file
184
elixir/apps/web/assets/vendor/status_page.js
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
// StatusPage API Wrapper. Vendored and reviewed by @jamilbk from https://cdn.statuspage.io/se-v2.js
|
||||
;(StatusPage = "undefined" == typeof StatusPage ? {} : StatusPage),
|
||||
(StatusPage.page = function (e) {
|
||||
if (!(e = e || {}).page)
|
||||
throw new Error("A pageId is required to initialize.")
|
||||
;(this.apiKey = e.apiKey || null),
|
||||
(this.error = e.error || this.error),
|
||||
(this.format = e.format || "json"),
|
||||
(this.pageId = e.page),
|
||||
(this.version = e.version || "v2"),
|
||||
(this.secure = !("secure" in e) || e.secure),
|
||||
(this.protocol = this.secure ? "https" : "http"),
|
||||
(this.host = e.host || "statuspage.io"),
|
||||
(this.host_with_port_and_protocol = e.test
|
||||
? ""
|
||||
: this.protocol + "://" + this.pageId + "." + this.host)
|
||||
}),
|
||||
(StatusPage.page.prototype.serialize = function (e, t) {
|
||||
var s = [],
|
||||
r = { sms: "email_sms", webhook: "endpoint" }
|
||||
for (var o in e)
|
||||
if ("to_sentence" !== o) {
|
||||
var i = o
|
||||
o = o in r ? r[o] : o
|
||||
var a = t ? t + "[" + o + "]" : o,
|
||||
n = e[i]
|
||||
s.push(
|
||||
"object" == typeof n
|
||||
? this.serialize(n, a)
|
||||
: encodeURIComponent(a) + "=" + encodeURIComponent(n)
|
||||
)
|
||||
}
|
||||
return s.join("&")
|
||||
}),
|
||||
(StatusPage.page.prototype.createStatusPageCORSRequest = function (e, t) {
|
||||
var s = new XMLHttpRequest()
|
||||
return (
|
||||
"withCredentials" in s
|
||||
? s.open(e, t, !0)
|
||||
: "undefined" != typeof XDomainRequest
|
||||
? (s = new XDomainRequest()).open(e, t)
|
||||
: (s = null),
|
||||
s
|
||||
)
|
||||
}),
|
||||
(StatusPage.page.prototype.executeRequestAndCallbackWithResponse = function (
|
||||
e
|
||||
) {
|
||||
if (!e.path) throw new Error("A path is required to make a request")
|
||||
var t = e.path,
|
||||
s = e.method || "GET",
|
||||
r = e.success || null,
|
||||
o = e.error || this.error,
|
||||
i =
|
||||
this.host_with_port_and_protocol +
|
||||
"/api/" +
|
||||
this.version +
|
||||
"/" +
|
||||
t +
|
||||
"." +
|
||||
this.format,
|
||||
a = this.createStatusPageCORSRequest(s, i)
|
||||
if (a)
|
||||
if (
|
||||
(this.apiKey &&
|
||||
(console.log(
|
||||
"!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!"
|
||||
),
|
||||
console.log(
|
||||
"!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!"
|
||||
),
|
||||
console.log(
|
||||
"!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!"
|
||||
),
|
||||
a.setRequestHeader("Authorization", "OAuth " + this.apiKey)),
|
||||
(a.onload = function () {
|
||||
var e = JSON.parse(a.responseText)
|
||||
r && r(e)
|
||||
}),
|
||||
(a.onerror = o),
|
||||
"POST" === s || "DELETE" === s)
|
||||
) {
|
||||
var n = e.data || {}
|
||||
a.setRequestHeader("Content-type", "application/x-www-form-urlencoded"),
|
||||
a.send(this.serialize(n))
|
||||
} else a.send()
|
||||
}),
|
||||
(StatusPage.page.prototype.get = function (e, t) {
|
||||
if (((t = t || {}), !e)) throw new Error("Path is required.")
|
||||
if (!t.success) throw new Error("Success Callback is required.")
|
||||
var s = t.success || {},
|
||||
r = t.error || {}
|
||||
this.executeRequestAndCallbackWithResponse({
|
||||
path: e,
|
||||
success: s,
|
||||
error: r,
|
||||
method: "GET",
|
||||
})
|
||||
}),
|
||||
(StatusPage.page.prototype.post = function (e, t) {
|
||||
if (((t = t || {}), !e)) throw new Error("Path is required.")
|
||||
var s = {}
|
||||
if ("subscribers" === e) {
|
||||
if (!t.subscriber) throw new Error("Subscriber is required to post.")
|
||||
s.subscriber = t.subscriber
|
||||
} else {
|
||||
if (!t.data) throw new Error("Data is required to post.")
|
||||
s = t.data
|
||||
}
|
||||
var r = t.success || {},
|
||||
o = t.error || {}
|
||||
this.executeRequestAndCallbackWithResponse({
|
||||
data: s,
|
||||
path: e,
|
||||
success: r,
|
||||
error: o,
|
||||
method: "POST",
|
||||
})
|
||||
}),
|
||||
(StatusPage.page.prototype["delete"] = function (e, t) {
|
||||
if (((t = t || {}), !e)) throw new Error("Path is required.")
|
||||
if (!t.subscriber) throw new Error("Data is required to delete.")
|
||||
var s = {}
|
||||
"subscribers" === e ? (s.subscriber = t.subscriber) : (s = t.data)
|
||||
var r = t.success || {},
|
||||
o = t.error || {}
|
||||
this.executeRequestAndCallbackWithResponse({
|
||||
data: s,
|
||||
path: e,
|
||||
success: r,
|
||||
error: o,
|
||||
method: "DELETE",
|
||||
})
|
||||
}),
|
||||
(StatusPage.page.prototype.error = function () {
|
||||
console.log("There was an error with your request")
|
||||
}),
|
||||
(StatusPage.page.prototype.summary = function (e) {
|
||||
this.get("summary", e)
|
||||
}),
|
||||
(StatusPage.page.prototype.status = function (e) {
|
||||
this.get("status", e)
|
||||
}),
|
||||
(StatusPage.page.prototype.components = function (e) {
|
||||
this.get("components", e)
|
||||
}),
|
||||
(StatusPage.page.prototype.incidents = function (e) {
|
||||
switch (e.filter) {
|
||||
case "unresolved":
|
||||
this.get("incidents/unresolved", e)
|
||||
break
|
||||
case "resolved":
|
||||
this.get("incidents/resolved", e)
|
||||
break
|
||||
default:
|
||||
this.get("incidents", e)
|
||||
}
|
||||
}),
|
||||
(StatusPage.page.prototype.scheduled_maintenances = function (e) {
|
||||
switch (e.filter) {
|
||||
case "active":
|
||||
this.get("scheduled-maintenances/active", e)
|
||||
break
|
||||
case "upcoming":
|
||||
this.get("scheduled-maintenances/upcoming", e)
|
||||
break
|
||||
default:
|
||||
this.get("scheduled-maintenances", e)
|
||||
}
|
||||
}),
|
||||
(StatusPage.page.prototype.subscribe = function (e) {
|
||||
if (!e || !e.subscriber) throw new Error("A subscriber object is required.")
|
||||
this.post("subscribers", e)
|
||||
}),
|
||||
(StatusPage.page.prototype.unsubscribe = function (e) {
|
||||
if (!e || !e.subscriber) throw new Error("A subscriber object is required.")
|
||||
if (!e.subscriber.id)
|
||||
throw new Error(
|
||||
"You must supply a subscriber.id in order to cancel a subscription."
|
||||
)
|
||||
this["delete"]("subscribers", e)
|
||||
})
|
||||
|
||||
export default StatusPage
|
||||
@@ -1037,6 +1037,16 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
def status_page_widget(assigns) do
|
||||
~H"""
|
||||
<div class="absolute bottom-0 left-0 justify-left p-4 space-x-4 w-full lg:flex bg-white dark:bg-gray-800 z-20">
|
||||
<.link href="https://firezone.statuspage.io" class="text-xs hover:underline">
|
||||
<span id="status-page-widget" phx-hook="StatusPage" />
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
## JS Commands
|
||||
|
||||
def show(js \\ %JS{}, selector) do
|
||||
|
||||
@@ -276,13 +276,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 justify-left p-4 space-x-4 w-full lg:flex bg-white dark:bg-gray-800 z-20">
|
||||
<a href="#" class="text-xs hover:underline">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
All systems online
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<.status_page_widget />
|
||||
</aside>
|
||||
|
||||
<main class="md:ml-64 h-auto pt-16">
|
||||
|
||||
@@ -53,7 +53,8 @@ config :web, Web.Plugs.SecureHeaders,
|
||||
"default-src 'self' 'nonce-${nonce}'",
|
||||
"img-src 'self' data: https://www.gravatar.com",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"script-src 'self' 'unsafe-inline'"
|
||||
"script-src 'self' 'unsafe-inline'",
|
||||
"connect-src 'self' data: https://firezone.statuspage.io"
|
||||
]
|
||||
|
||||
###############################
|
||||
|
||||
Reference in New Issue
Block a user