mirror of
https://github.com/TwiN/gatus.git
synced 2025-04-10 19:08:28 +02:00
Start working on migrating frontend to Vue 3
This commit is contained in:
parent
f1aa5191bf
commit
dc6cb8fc1d
23
web/app/.gitignore
vendored
Normal file
23
web/app/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
24
web/app/README.md
Normal file
24
web/app/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# app
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
web/app/babel.config.js
Normal file
5
web/app/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
27924
web/app/package-lock.json
generated
Normal file
27924
web/app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
web/app/package.json
Normal file
46
web/app/package.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/postcss7-compat": "^2.0.2",
|
||||||
|
"autoprefixer": "^9.8.6",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"postcss": "^7.0.35",
|
||||||
|
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"@vue/compiler-sfc": "^3.0.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^7.0.0-0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
5
web/app/postcss.config.js
Normal file
5
web/app/postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require('tailwindcss')
|
||||||
|
],
|
||||||
|
};
|
BIN
web/app/public/favicon.ico
Normal file
BIN
web/app/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
17
web/app/public/index.html
Normal file
17
web/app/public/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!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.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>Health Dashboard | Gatus</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
52
web/app/src/App.vue
Normal file
52
web/app/src/App.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<Services :serviceStatuses="serviceStatuses" :maximumNumberOfResults="20" :showStatusOnHover="true" />
|
||||||
|
<Social />
|
||||||
|
<Settings @refreshStatuses="fetchStatuses" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Social from './components/Social.vue'
|
||||||
|
import Settings from './components/Settings.vue'
|
||||||
|
import Services from './components/Services.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
Services,
|
||||||
|
Social,
|
||||||
|
Settings
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchStatuses() {
|
||||||
|
console.log("[App][fetchStatuses] Fetching statuses");
|
||||||
|
fetch("http://localhost:8080/api/v1/statuses")
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (JSON.stringify(this.serviceStatuses) !== JSON.stringify(data)) {
|
||||||
|
console.log(data);
|
||||||
|
this.serviceStatuses = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
serviceStatuses: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchStatuses();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
background-color: #f7f9fb;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
BIN
web/app/src/assets/github.png
Normal file
BIN
web/app/src/assets/github.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
web/app/src/assets/logo.png
Normal file
BIN
web/app/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
128
web/app/src/components/Service.vue
Normal file
128
web/app/src/components/Service.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<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'>{{ data.name }}</span> <span class='text-gray-500 font-light'>- {{ data.results[data.results.length - 1].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>
|
||||||
|
<div class='status-over-time flex flex-row'>
|
||||||
|
<slot v-for="filler in 20 - data.results.length" :key="filler">
|
||||||
|
<span class="status rounded border border-dashed"> </span>
|
||||||
|
</slot>
|
||||||
|
<slot v-for="result in data.results" :key="result">
|
||||||
|
<span v-if="result.success" class="status rounded bg-success">✓</span>
|
||||||
|
<span v-else class="status rounded bg-red-600">X</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='flex flex-wrap status-time-ago'>
|
||||||
|
<div class='w-1/2'>
|
||||||
|
{{ generatePrettyTimeAgo(data.results[0].timestamp) }}
|
||||||
|
</div>
|
||||||
|
<div class='w-1/2 text-right'>
|
||||||
|
{{ generatePrettyTimeAgo(data.results[data.results.length - 1].timestamp) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Service',
|
||||||
|
props: {
|
||||||
|
data: Object
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateMinAndMaxResponseTimes() {
|
||||||
|
let minResponseTime = null;
|
||||||
|
let maxResponseTime = null;
|
||||||
|
for (let i in this.data.results) {
|
||||||
|
const responseTime = parseInt(this.data.results[i].duration/1000000);
|
||||||
|
if (minResponseTime == null || minResponseTime > responseTime) {
|
||||||
|
minResponseTime = responseTime;
|
||||||
|
}
|
||||||
|
if (maxResponseTime == null || maxResponseTime < responseTime) {
|
||||||
|
maxResponseTime = responseTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.minResponseTime !== minResponseTime) {
|
||||||
|
this.minResponseTime = minResponseTime;
|
||||||
|
}
|
||||||
|
if (this.maxResponseTime !== maxResponseTime) {
|
||||||
|
this.maxResponseTime = maxResponseTime;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data: function () {
|
||||||
|
this.updateMinAndMaxResponseTimes();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.updateMinAndMaxResponseTimes()
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
minResponseTime: 0,
|
||||||
|
maxResponseTime: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.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(:first-child) {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-time-ago {
|
||||||
|
color: #6a737d;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-min-max-ms {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
80
web/app/src/components/ServiceGroup.vue
Normal file
80
web/app/src/components/ServiceGroup.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="services.length === 0 ? 'mt-3' : 'mt-4'">
|
||||||
|
<slot v-if="name !== 'undefined'">
|
||||||
|
<div class="service-group container pt-2 border">
|
||||||
|
<h5 class='text-monospace text-gray-400 text-xl font-medium pb-2 px-3'>
|
||||||
|
<span v-if="healthy" class='text-green-600'>✓</span>
|
||||||
|
<span v-else class='text-yellow-400'>~</span>
|
||||||
|
{{ name }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
<div :class="name === 'undefined' ? '' : 'service-group-content'">
|
||||||
|
<slot v-for="service in services" :key="service">
|
||||||
|
<Service :data="service"/>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Service from './Service.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ServiceGroup',
|
||||||
|
components: {
|
||||||
|
Service
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
services: Array
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
healthCheck() {
|
||||||
|
if (this.services) {
|
||||||
|
for (let i in this.services) {
|
||||||
|
for (let j in this.services[i].results) {
|
||||||
|
if (!this.services[i].results[j].success) {
|
||||||
|
// Set the service group to unhealthy (only if it's currently healthy)
|
||||||
|
if (this.healthy) {
|
||||||
|
this.healthy = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set the service group to healthy (only if it's currently unhealthy)
|
||||||
|
if (!this.healthy) {
|
||||||
|
this.healthy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
services: function () {
|
||||||
|
this.healthCheck();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.healthCheck();
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
healthy: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.service-group {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-group h5:hover {
|
||||||
|
color: #1b1e21 !important;
|
||||||
|
}
|
||||||
|
</style>
|
100
web/app/src/components/Services.vue
Normal file
100
web/app/src/components/Services.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container mx-auto rounded shadow-xl border my-3 p-5 text-left" 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="../assets/logo.png" alt="Gatus" style="min-width: 50px; max-width: 200px; width: 20%;"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="results">
|
||||||
|
<slot v-for="serviceGroup in serviceGroups" :key="serviceGroup">
|
||||||
|
<ServiceGroup :services="serviceGroup.services" :name="serviceGroup.name" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ServiceGroup from './ServiceGroup.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Services',
|
||||||
|
components: {
|
||||||
|
ServiceGroup
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
maximumNumberOfResults: Number,
|
||||||
|
showStatusOnHover: Boolean,
|
||||||
|
serviceStatuses: Object
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
process() {
|
||||||
|
let outputByGroup = {};
|
||||||
|
for (let serviceStatusIndex in this.serviceStatuses) {
|
||||||
|
let serviceStatus = this.serviceStatuses[serviceStatusIndex];
|
||||||
|
// create an empty entry if this group is new
|
||||||
|
if (!outputByGroup[serviceStatus.group] || outputByGroup[serviceStatus.group].length === 0) {
|
||||||
|
outputByGroup[serviceStatus.group] = [];
|
||||||
|
}
|
||||||
|
outputByGroup[serviceStatus.group].push(serviceStatus);
|
||||||
|
}
|
||||||
|
let serviceGroups = [];
|
||||||
|
for (let name in outputByGroup) {
|
||||||
|
if (name !== 'undefined') {
|
||||||
|
serviceGroups.push({ name: name, services: outputByGroup[name]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add all services that don't have a group at the end
|
||||||
|
if (outputByGroup['undefined']) {
|
||||||
|
serviceGroups.push({name: 'undefined', services: outputByGroup['undefined']})
|
||||||
|
}
|
||||||
|
this.serviceGroups = serviceGroups;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
serviceStatuses: function () {
|
||||||
|
this.process();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
userClickedStatus: false,
|
||||||
|
serviceGroups: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
66
web/app/src/components/Settings.vue
Normal file
66
web/app/src/components/Settings.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div id="settings">
|
||||||
|
<div class="flex bg-gray-200 rounded border border-gray-300 shadow">
|
||||||
|
<div class="text-sm text-gray-600 rounded-xl py-1 px-2">
|
||||||
|
↻
|
||||||
|
</div>
|
||||||
|
<select class="text-center text-gray-500 text-sm" id="refresh-rate" ref="refreshInterval" @change="handleChangeRefreshInterval">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Settings',
|
||||||
|
props: {},
|
||||||
|
methods: {
|
||||||
|
setRefreshInterval(seconds) {
|
||||||
|
let that = this;
|
||||||
|
this.refreshIntervalHandler = setInterval(function() {
|
||||||
|
that.refreshStatuses();
|
||||||
|
}, seconds * 1000);
|
||||||
|
},
|
||||||
|
refreshStatuses() {
|
||||||
|
this.$emit('refreshStatuses')
|
||||||
|
},
|
||||||
|
handleChangeRefreshInterval() {
|
||||||
|
this.refreshStatuses();
|
||||||
|
clearInterval(this.refreshIntervalHandler);
|
||||||
|
this.setRefreshInterval(this.$refs.refreshInterval.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.setRefreshInterval(this.refreshInterval);
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
refreshInterval: 30,
|
||||||
|
refreshIntervalHandler: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// props.refreshInterval = 30
|
||||||
|
//$("#refresh-rate").val(30);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#settings {
|
||||||
|
position: fixed;
|
||||||
|
left: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
#settings select:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
</style>
|
32
web/app/src/components/Social.vue
Normal file
32
web/app/src/components/Social.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div id="social">
|
||||||
|
<a href="https://github.com/TwinProduction/gatus" target="_blank" title="Gatus on GitHub">
|
||||||
|
<img src="../assets/github.png" alt="GitHub" width="32" height="auto" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Social'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#social {
|
||||||
|
position: fixed;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
#social img {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
#social img:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
11
web/app/src/index.css
Normal file
11
web/app/src/index.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.bg-success {
|
||||||
|
background-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-monospace {
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
}
|
5
web/app/src/main.js
Normal file
5
web/app/src/main.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
11
web/app/tailwind.config.js
Normal file
11
web/app/tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
|
darkMode: false, // or 'media' or 'class'
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user