2020-05-25 22:07:03 +00:00
|
|
|
<template>
|
|
|
|
<div
|
|
|
|
id="app"
|
|
|
|
v-if="config"
|
|
|
|
:class="[
|
|
|
|
`theme-${config.theme}`,
|
|
|
|
isDark ? 'is-dark' : 'is-light',
|
2020-05-30 01:21:32 +00:00
|
|
|
!config.footer ? 'no-footer' : '',
|
2020-05-25 22:07:03 +00:00
|
|
|
]"
|
|
|
|
>
|
|
|
|
<DynamicTheme :themes="config.colors" />
|
|
|
|
<div id="bighead">
|
|
|
|
<section v-if="config.header" class="first-line">
|
|
|
|
<div v-cloak class="container">
|
|
|
|
<div class="logo">
|
2021-03-07 06:50:58 +00:00
|
|
|
<a href="#">
|
|
|
|
<img v-if="config.logo" :src="config.logo" alt="dashboard logo" />
|
|
|
|
</a>
|
2020-07-22 20:06:26 +00:00
|
|
|
<i v-if="config.icon" :class="config.icon"></i>
|
2020-05-25 22:07:03 +00:00
|
|
|
</div>
|
|
|
|
<div class="dashboard-title">
|
|
|
|
<span class="headline">{{ config.subtitle }}</span>
|
|
|
|
<h1>{{ config.title }}</h1>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
<Navbar
|
|
|
|
:open="showMenu"
|
|
|
|
:links="config.links"
|
2020-10-23 22:24:16 +00:00
|
|
|
@navbar-toggle="showMenu = !showMenu"
|
2020-05-25 22:07:03 +00:00
|
|
|
>
|
|
|
|
<DarkMode @updated="isDark = $event" />
|
|
|
|
|
|
|
|
<SettingToggle
|
|
|
|
@updated="vlayout = $event"
|
|
|
|
name="vlayout"
|
|
|
|
icon="fa-list"
|
|
|
|
iconAlt="fa-columns"
|
|
|
|
/>
|
|
|
|
|
|
|
|
<SearchInput
|
|
|
|
class="navbar-item is-inline-block-mobile"
|
2021-12-12 15:28:20 +00:00
|
|
|
:hotkey="searchHotkey()"
|
2020-05-25 22:07:03 +00:00
|
|
|
@input="filterServices"
|
2020-10-23 22:24:16 +00:00
|
|
|
@search-focus="showMenu = true"
|
|
|
|
@search-open="navigateToFirstService"
|
|
|
|
@search-cancel="filterServices"
|
2020-05-25 22:07:03 +00:00
|
|
|
/>
|
|
|
|
</Navbar>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<section id="main-section" class="section">
|
|
|
|
<div v-cloak class="container">
|
2020-06-10 02:24:43 +00:00
|
|
|
<ConnectivityChecker
|
|
|
|
v-if="config.connectivityCheck"
|
2020-10-23 22:24:16 +00:00
|
|
|
@network-status-update="offline = $event"
|
2020-06-10 02:24:43 +00:00
|
|
|
/>
|
2020-05-25 22:07:03 +00:00
|
|
|
<div v-if="!offline">
|
|
|
|
<!-- Optional messages -->
|
|
|
|
<Message :item="config.message" />
|
|
|
|
|
|
|
|
<!-- Horizontal layout -->
|
|
|
|
<div v-if="!vlayout || filter" class="columns is-multiline">
|
|
|
|
<template v-for="group in services">
|
|
|
|
<h2 v-if="group.name" class="column is-full group-title">
|
2020-06-25 22:54:18 +00:00
|
|
|
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
2021-03-06 12:56:44 +00:00
|
|
|
<div v-else-if="group.logo" class="group-logo media-left">
|
|
|
|
<figure class="image is-48x48">
|
|
|
|
<img :src="group.logo" :alt="`${group.name} logo`" />
|
|
|
|
</figure>
|
|
|
|
</div>
|
2020-05-25 22:07:03 +00:00
|
|
|
{{ group.name }}
|
|
|
|
</h2>
|
|
|
|
<Service
|
2020-11-29 19:43:08 +00:00
|
|
|
v-for="(item, index) in group.items"
|
|
|
|
:key="index"
|
2021-10-10 07:26:02 +00:00
|
|
|
:item="item"
|
|
|
|
:proxy="config.proxy"
|
2020-06-10 04:54:13 +00:00
|
|
|
:class="['column', `is-${12 / config.columns}`]"
|
2020-05-25 22:07:03 +00:00
|
|
|
/>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Vertical layout -->
|
|
|
|
<div
|
|
|
|
v-if="!filter && vlayout"
|
|
|
|
class="columns is-multiline layout-vertical"
|
|
|
|
>
|
|
|
|
<div
|
2020-06-10 04:54:13 +00:00
|
|
|
:class="['column', `is-${12 / config.columns}`]"
|
2020-05-25 22:07:03 +00:00
|
|
|
v-for="group in services"
|
|
|
|
:key="group.name"
|
|
|
|
>
|
|
|
|
<h2 v-if="group.name" class="group-title">
|
2020-06-25 22:54:18 +00:00
|
|
|
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
2021-03-06 12:56:44 +00:00
|
|
|
<div v-else-if="group.logo" class="group-logo media-left">
|
|
|
|
<figure class="image is-48x48">
|
|
|
|
<img :src="group.logo" :alt="`${group.name} logo`" />
|
|
|
|
</figure>
|
|
|
|
</div>
|
2020-05-25 22:07:03 +00:00
|
|
|
{{ group.name }}
|
|
|
|
</h2>
|
|
|
|
<Service
|
2020-11-29 19:43:08 +00:00
|
|
|
v-for="(item, index) in group.items"
|
|
|
|
:key="index"
|
2021-10-10 07:26:02 +00:00
|
|
|
:item="item"
|
|
|
|
:proxy="config.proxy"
|
2020-05-25 22:07:03 +00:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
<footer class="footer">
|
|
|
|
<div class="container">
|
|
|
|
<div
|
|
|
|
class="content has-text-centered"
|
|
|
|
v-if="config.footer"
|
|
|
|
v-html="config.footer"
|
|
|
|
></div>
|
|
|
|
</div>
|
|
|
|
</footer>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
const jsyaml = require("js-yaml");
|
|
|
|
const merge = require("lodash.merge");
|
|
|
|
|
|
|
|
import Navbar from "./components/Navbar.vue";
|
|
|
|
import ConnectivityChecker from "./components/ConnectivityChecker.vue";
|
|
|
|
import Service from "./components/Service.vue";
|
|
|
|
import Message from "./components/Message.vue";
|
|
|
|
import SearchInput from "./components/SearchInput.vue";
|
|
|
|
import SettingToggle from "./components/SettingToggle.vue";
|
|
|
|
import DarkMode from "./components/DarkMode.vue";
|
|
|
|
import DynamicTheme from "./components/DynamicTheme.vue";
|
|
|
|
|
|
|
|
import defaultConfig from "./assets/defaults.yml";
|
|
|
|
|
|
|
|
export default {
|
|
|
|
name: "App",
|
|
|
|
components: {
|
|
|
|
Navbar,
|
|
|
|
ConnectivityChecker,
|
|
|
|
Service,
|
|
|
|
Message,
|
|
|
|
SearchInput,
|
|
|
|
SettingToggle,
|
|
|
|
DarkMode,
|
2020-05-30 01:21:32 +00:00
|
|
|
DynamicTheme,
|
2020-05-25 22:07:03 +00:00
|
|
|
},
|
|
|
|
data: function () {
|
|
|
|
return {
|
|
|
|
config: null,
|
|
|
|
services: null,
|
|
|
|
offline: false,
|
|
|
|
filter: "",
|
|
|
|
vlayout: true,
|
|
|
|
isDark: null,
|
2020-05-30 01:21:32 +00:00
|
|
|
showMenu: false,
|
2020-05-25 22:07:03 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
created: async function () {
|
2021-03-07 06:50:58 +00:00
|
|
|
this.buildDashboard();
|
|
|
|
window.onhashchange = this.buildDashboard;
|
2020-05-25 22:07:03 +00:00
|
|
|
},
|
|
|
|
methods: {
|
2021-10-12 12:36:22 +00:00
|
|
|
searchHotkey() {
|
|
|
|
if (this.config.hotkey && this.config.hotkey.search) {
|
|
|
|
return this.config.hotkey.search;
|
|
|
|
}
|
|
|
|
},
|
2021-03-07 06:50:58 +00:00
|
|
|
buildDashboard: async function () {
|
|
|
|
const defaults = jsyaml.load(defaultConfig);
|
|
|
|
let config;
|
|
|
|
try {
|
|
|
|
config = await this.getConfig();
|
|
|
|
const path =
|
|
|
|
window.location.hash.substring(1) != ""
|
|
|
|
? window.location.hash.substring(1)
|
|
|
|
: null;
|
|
|
|
|
|
|
|
if (path) {
|
|
|
|
let pathConfig = await this.getConfig(`assets/${path}.yml`); // the slash (/) is included in the pathname
|
|
|
|
config = Object.assign(config, pathConfig);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.log(error);
|
|
|
|
config = this.handleErrors("⚠️ Error loading configuration", error);
|
|
|
|
}
|
|
|
|
this.config = merge(defaults, config);
|
|
|
|
this.services = this.config.services;
|
|
|
|
document.title =
|
|
|
|
this.config.documentTitle ||
|
|
|
|
`${this.config.title} | ${this.config.subtitle}`;
|
|
|
|
if (this.config.stylesheet) {
|
|
|
|
let stylesheet = "";
|
|
|
|
for (const file of this.config.stylesheet) {
|
|
|
|
stylesheet += `@import "${file}";`;
|
|
|
|
}
|
|
|
|
this.createStylesheet(stylesheet);
|
|
|
|
}
|
|
|
|
},
|
2020-06-24 05:56:33 +00:00
|
|
|
getConfig: function (path = "assets/config.yml") {
|
2020-06-10 02:11:42 +00:00
|
|
|
return fetch(path).then((response) => {
|
2020-07-13 16:16:47 +00:00
|
|
|
if (response.redirected) {
|
|
|
|
// This allows to work with authentication proxies.
|
|
|
|
window.location.href = response.url;
|
|
|
|
return;
|
|
|
|
}
|
2022-02-10 21:07:00 +00:00
|
|
|
|
2020-06-10 02:11:42 +00:00
|
|
|
if (!response.ok) {
|
2020-07-13 16:16:47 +00:00
|
|
|
throw Error(`${response.statusText}: ${response.body}`);
|
2020-06-10 02:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const that = this;
|
|
|
|
return response
|
|
|
|
.text()
|
|
|
|
.then((body) => {
|
2020-06-06 23:57:36 +00:00
|
|
|
return jsyaml.load(body);
|
2020-06-10 02:11:42 +00:00
|
|
|
})
|
|
|
|
.then(function (config) {
|
|
|
|
if (config.externalConfig) {
|
|
|
|
return that.getConfig(config.externalConfig);
|
|
|
|
}
|
|
|
|
return config;
|
2020-06-06 23:57:36 +00:00
|
|
|
});
|
2020-06-10 02:11:42 +00:00
|
|
|
});
|
2020-05-25 22:07:03 +00:00
|
|
|
},
|
|
|
|
matchesFilter: function (item) {
|
|
|
|
return (
|
|
|
|
item.name.toLowerCase().includes(this.filter) ||
|
2020-11-22 13:00:29 +00:00
|
|
|
(item.subtitle && item.subtitle.toLowerCase().includes(this.filter)) ||
|
2020-05-25 22:07:03 +00:00
|
|
|
(item.tag && item.tag.toLowerCase().includes(this.filter))
|
|
|
|
);
|
|
|
|
},
|
|
|
|
navigateToFirstService: function (target) {
|
|
|
|
try {
|
|
|
|
const service = this.services[0].items[0];
|
|
|
|
window.open(service.url, target || service.target || "_self");
|
|
|
|
} catch (error) {
|
|
|
|
console.warning("fail to open service");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
filterServices: function (filter) {
|
|
|
|
this.filter = filter;
|
|
|
|
|
|
|
|
if (!filter) {
|
|
|
|
this.services = this.config.services;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const searchResultItems = [];
|
|
|
|
for (const group of this.config.services) {
|
|
|
|
for (const item of group.items) {
|
|
|
|
if (this.matchesFilter(item)) {
|
|
|
|
searchResultItems.push(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.services = [
|
|
|
|
{
|
|
|
|
name: filter,
|
|
|
|
icon: "fas fa-search",
|
2020-05-30 01:21:32 +00:00
|
|
|
items: searchResultItems,
|
|
|
|
},
|
2020-05-25 22:07:03 +00:00
|
|
|
];
|
2020-05-30 01:21:32 +00:00
|
|
|
},
|
2020-06-06 23:57:36 +00:00
|
|
|
handleErrors: function (title, content) {
|
|
|
|
return {
|
|
|
|
message: {
|
|
|
|
title: title,
|
|
|
|
style: "is-danger",
|
|
|
|
content: content,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
},
|
2020-09-04 22:43:44 +00:00
|
|
|
createStylesheet: function (css) {
|
|
|
|
let style = document.createElement("style");
|
2020-07-22 20:42:43 +00:00
|
|
|
style.appendChild(document.createTextNode(css));
|
|
|
|
document.head.appendChild(style);
|
|
|
|
},
|
2020-05-30 01:21:32 +00:00
|
|
|
},
|
2020-05-25 22:07:03 +00:00
|
|
|
};
|
|
|
|
</script>
|