mirror of
https://github.com/Lissy93/dashy.git
synced 2024-12-24 01:12:06 +03:00
Adds an (optional) status check feature, plus some refactoring
This commit is contained in:
parent
195d433f75
commit
0b1f66b7b7
42
src/App.vue
42
src/App.vue
@ -12,7 +12,7 @@ import Header from '@/components/PageStrcture/Header.vue';
|
||||
import Footer from '@/components/PageStrcture/Footer.vue';
|
||||
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
|
||||
import Defaults, { localStorageKeys, splashScreenTime } from '@/utils/defaults';
|
||||
import conf from '../public/conf.yml';
|
||||
import { config, appConfig, pageInfo } from '@/utils/ConfigAccumalator';
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
@ -21,48 +21,18 @@ export default {
|
||||
Footer,
|
||||
LoadingScreen,
|
||||
},
|
||||
provide: {
|
||||
config,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// pageInfo: this.getPageInfo(conf.pageInfo),
|
||||
showFooter: Defaults.visibleComponents.footer,
|
||||
isLoading: true,
|
||||
appConfig,
|
||||
pageInfo,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pageInfo() {
|
||||
return this.getPageInfo(conf.pageInfo);
|
||||
},
|
||||
appConfig() {
|
||||
if (localStorage[localStorageKeys.APP_CONFIG]) {
|
||||
return JSON.parse(localStorage[localStorageKeys.APP_CONFIG]);
|
||||
} else if (conf.appConfig) {
|
||||
return conf.appConfig;
|
||||
} else {
|
||||
return Defaults.appConfig;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Returns either page info from the config, or default values */
|
||||
getPageInfo(pageInfo) {
|
||||
const defaults = Defaults.pageInfo;
|
||||
|
||||
let localPageInfo;
|
||||
try {
|
||||
localPageInfo = JSON.parse(localStorage[localStorageKeys.PAGE_INFO]);
|
||||
} catch (e) {
|
||||
localPageInfo = {};
|
||||
}
|
||||
if (pageInfo) {
|
||||
return {
|
||||
title: localPageInfo.title || pageInfo.title || defaults.title,
|
||||
description: localPageInfo.description || pageInfo.description || defaults.description,
|
||||
navLinks: localPageInfo.navLinks || pageInfo.navLinks || defaults.navLinks,
|
||||
footerText: localPageInfo.footerText || pageInfo.footerText || defaults.footerText,
|
||||
};
|
||||
}
|
||||
return defaults;
|
||||
},
|
||||
getFooterText() {
|
||||
if (this.pageInfo && this.pageInfo.footerText) {
|
||||
return this.pageInfo.footerText;
|
||||
|
@ -20,12 +20,20 @@
|
||||
<!-- Small icon, showing opening method on hover -->
|
||||
<ItemOpenMethodIcon class="opening-method-icon" :isSmall="!icon" :openingMethod="target"
|
||||
:position="itemSize === 'medium'? 'bottom right' : 'top right'"/>
|
||||
<StatusIndicator
|
||||
class="status-indicator"
|
||||
v-if="enableStatusCheck"
|
||||
:statusSuccess="statusResponse ? statusResponse.successStatus : undefined"
|
||||
:statusText="statusResponse ? statusResponse.message : undefined"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Icon from '@/components/LinkItems/ItemIcon.vue';
|
||||
import ItemOpenMethodIcon from '@/components/LinkItems/ItemOpenMethodIcon';
|
||||
import StatusIndicator from '@/components/LinkItems/StatusIndicator';
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
@ -44,6 +52,7 @@ export default {
|
||||
validator: (value) => ['newtab', 'sametab', 'iframe'].indexOf(value) !== -1,
|
||||
},
|
||||
itemSize: String,
|
||||
enableStatusCheck: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -52,11 +61,13 @@ export default {
|
||||
color: this.color,
|
||||
background: this.backgroundColor,
|
||||
},
|
||||
statusResponse: undefined,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
ItemOpenMethodIcon,
|
||||
StatusIndicator,
|
||||
},
|
||||
methods: {
|
||||
/* Called when an item is clicked, manages the opening of iframe & resets the search field */
|
||||
@ -88,9 +99,11 @@ export default {
|
||||
trigger: 'hover focus',
|
||||
hideOnTargetClick: true,
|
||||
html: false,
|
||||
placement: this.statusResponse ? 'left' : 'auto',
|
||||
delay: { show: 600, hide: 200 },
|
||||
};
|
||||
},
|
||||
/* Used by certain themes, which display an icon with animated CSS */
|
||||
getUnicodeOpeningIcon() {
|
||||
switch (this.target) {
|
||||
case 'newtab': return '"\\f360"';
|
||||
@ -99,9 +112,24 @@ export default {
|
||||
default: return '"\\f054"';
|
||||
}
|
||||
},
|
||||
checkWebsiteStatus() {
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
const endpoint = `${baseUrl}/ping?url=${this.url}`;
|
||||
axios.get(endpoint)
|
||||
.then((response) => {
|
||||
if (response.data) this.statusResponse = response.data;
|
||||
})
|
||||
.catch(() => {
|
||||
this.statusResponse = {
|
||||
statusText: 'Failed to make request',
|
||||
statusSuccess: false,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.manageTitleEllipse();
|
||||
if (this.enableStatusCheck) this.checkWebsiteStatus();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -122,6 +150,7 @@ export default {
|
||||
box-shadow: var(--item-shadow);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
&:hover {
|
||||
box-shadow: var(--item-hover-shadow);
|
||||
background: var(--item-background-hover);
|
||||
@ -175,6 +204,13 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* Colored dot showing service status */
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.opening-method-icon {
|
||||
display: none; // Hidden by default, visible on hover
|
||||
}
|
||||
|
@ -28,6 +28,7 @@
|
||||
:color="item.color"
|
||||
:backgroundColor="item.backgroundColor"
|
||||
:itemSize="newItemSize"
|
||||
:enableStatusCheck="shouldEnableStatusCheck(item.statusCheck)"
|
||||
@itemClicked="$emit('itemClicked')"
|
||||
@triggerModal="triggerModal"
|
||||
/>
|
||||
@ -49,6 +50,7 @@ import IframeModal from '@/components/LinkItems/IframeModal.vue';
|
||||
|
||||
export default {
|
||||
name: 'ItemGroup',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
groupId: String,
|
||||
title: String,
|
||||
@ -92,6 +94,10 @@ export default {
|
||||
modalChanged(changedTo) {
|
||||
this.$emit('change-modal-visibility', changedTo);
|
||||
},
|
||||
shouldEnableStatusCheck(itemPreference) {
|
||||
const globalPreference = this.config.appConfig.statusCheck || false;
|
||||
return itemPreference !== undefined ? itemPreference : globalPreference;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
122
src/components/LinkItems/StatusIndicator.vue
Normal file
122
src/components/LinkItems/StatusIndicator.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div
|
||||
v-tooltip="{
|
||||
content: statusText || otherStatusText, classes: ['status-tooltip', `tip-${color()}`] }"
|
||||
class="indicator"
|
||||
@click="showToast()">
|
||||
<div :class="`dot dot-${color()}`">
|
||||
<span><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'StatusIndicator',
|
||||
props: {
|
||||
statusText: String,
|
||||
statusSuccess: Boolean,
|
||||
},
|
||||
methods: {
|
||||
/* Returns a color, based on success status */
|
||||
color() {
|
||||
switch (this.statusSuccess) {
|
||||
case undefined: return ((new Date() - this.startTime) > 2000) ? 'grey' : 'yellow';
|
||||
case true: return 'green'; // Success!
|
||||
default: return 'red'; // Not success, therefore failure
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
startTime: new Date(), // Used for timeout
|
||||
otherStatusText: 'Checking...', // Used before server has responded
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
if (!this.statusText) this.otherStatusText = 'Request timed out';
|
||||
}, 2000);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.indicator {
|
||||
padding: 5px;
|
||||
transition: all .2s ease-in-out;
|
||||
cursor: help;
|
||||
&:hover {
|
||||
transform: scale(1.25);
|
||||
filter: saturate(2);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: .75; transform: scale(1); }
|
||||
25% { opacity: 0.75; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(1.8); }
|
||||
}
|
||||
@keyframes applyOpacity {
|
||||
50% { opacity: 0.9; }
|
||||
to { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.dot {
|
||||
border-radius: 50%;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
animation: applyOpacity 1s ease-in 8s forwards;
|
||||
> span, > span span, > span span:after {
|
||||
animation: pulse 1s linear 0.5s 2;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
content: '';
|
||||
}
|
||||
&.dot-green {
|
||||
background-color: var(--success);
|
||||
span, span:after {
|
||||
background-color: var(--success);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
&.dot-red {
|
||||
background-color: var(--danger);
|
||||
span, span:after {
|
||||
background-color: var(--danger);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
&.dot-yellow {
|
||||
background-color: var(--warning);
|
||||
span, span:after {
|
||||
background-color: var(--warning);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
&.dot-grey {
|
||||
background-color: var(--medium-grey);
|
||||
span, span:after {
|
||||
background-color: var(--medium-grey);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.status-tooltip {
|
||||
background: var(--background-darker) !important;
|
||||
font-size: 1rem;
|
||||
z-index: 10;
|
||||
&.tip-green { border: 1px solid var(--success); }
|
||||
&.tip-yellow { border: 1px solid var(--warning); }
|
||||
&.tip-red { border: 1px solid var(--danger); }
|
||||
}
|
||||
</style>
|
@ -1,12 +1,14 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
/* Import component Vue plugins, used throughout the app */
|
||||
import VTooltip from 'v-tooltip'; // A Vue directive for Popper.js, tooltip component
|
||||
import VModal from 'vue-js-modal'; // Modal component
|
||||
import VSelect from 'vue-select'; // Select dropdown component
|
||||
import VTabs from 'vue-material-tabs'; // Tab view component, used on the config page
|
||||
import Toasted from 'vue-toasted'; // Toast component, used to show confirmation notifications
|
||||
|
||||
import { toastedOptions } from './utils/defaults';
|
||||
import App from './App.vue';
|
||||
import Dashy from './App.vue';
|
||||
import router from './router';
|
||||
import './registerServiceWorker';
|
||||
|
||||
@ -20,5 +22,5 @@ Vue.config.productionTip = false;
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: (awesome) => awesome(App),
|
||||
render: (awesome) => awesome(Dashy),
|
||||
}).$mount('#app');
|
||||
|
@ -1,36 +1,15 @@
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import Home from './views/Home.vue';
|
||||
import Login from './views/Login.vue';
|
||||
import conf from '../public/conf.yml'; // Main site configuration
|
||||
import { pageInfo as defaultPageInfo, localStorageKeys } from './utils/defaults';
|
||||
import { isLoggedIn } from './utils/Auth';
|
||||
|
||||
import Home from '@/views/Home.vue';
|
||||
import Login from '@/views/Login.vue';
|
||||
import { isLoggedIn } from '@/utils/Auth';
|
||||
import { appConfig, pageInfo, sections } from '@/utils/ConfigAccumalator';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
const { sections, pageInfo, appConfig } = conf;
|
||||
let localPageInfo;
|
||||
try {
|
||||
localPageInfo = JSON.parse(localStorage[localStorageKeys.PAGE_INFO]);
|
||||
} catch (e) {
|
||||
localPageInfo = undefined;
|
||||
}
|
||||
|
||||
let localAppConfig;
|
||||
try {
|
||||
localAppConfig = JSON.parse(localStorage[localStorageKeys.APP_CONFIG]);
|
||||
} catch (e) {
|
||||
localAppConfig = undefined;
|
||||
}
|
||||
|
||||
const config = {
|
||||
sections: sections || [],
|
||||
pageInfo: localPageInfo || pageInfo || defaultPageInfo,
|
||||
appConfig: localAppConfig || appConfig || {},
|
||||
};
|
||||
|
||||
const isAuthenticated = () => {
|
||||
const users = config.appConfig.auth;
|
||||
const users = appConfig.auth;
|
||||
return (!users || isLoggedIn(users));
|
||||
};
|
||||
|
||||
@ -40,7 +19,11 @@ const router = new Router({
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home,
|
||||
props: config,
|
||||
props: {
|
||||
appConfig,
|
||||
pageInfo,
|
||||
sections,
|
||||
},
|
||||
meta: {
|
||||
title: pageInfo.title || 'Home Page',
|
||||
metaTags: [
|
||||
@ -56,7 +39,7 @@ const router = new Router({
|
||||
name: 'login',
|
||||
component: Login,
|
||||
props: {
|
||||
appConfig: config.appConfig,
|
||||
appConfig,
|
||||
},
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (isAuthenticated()) router.push({ path: '/' });
|
||||
|
58
src/utils/ConfigAccumalator.js
Normal file
58
src/utils/ConfigAccumalator.js
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Reads the users config from `conf.yml`, and combines it with any local preferences
|
||||
* Also ensures that any missing attributes are populated with defaults, and the
|
||||
* object is structurally sound, to avoid any error if the user is missing something
|
||||
* The main config object is make up of three parts: appConfig, pageInfo and sections
|
||||
*/
|
||||
import Defaults, { localStorageKeys } from '@/utils/defaults';
|
||||
import conf from '../../public/conf.yml';
|
||||
|
||||
export const appConfig = (() => {
|
||||
if (localStorage[localStorageKeys.APP_CONFIG]) {
|
||||
return JSON.parse(localStorage[localStorageKeys.APP_CONFIG]);
|
||||
} else if (conf.appConfig) {
|
||||
return conf.appConfig;
|
||||
} else {
|
||||
return Defaults.appConfig;
|
||||
}
|
||||
})();
|
||||
|
||||
export const pageInfo = (() => {
|
||||
const defaults = Defaults.pageInfo;
|
||||
let localPageInfo;
|
||||
try {
|
||||
localPageInfo = JSON.parse(localStorage[localStorageKeys.PAGE_INFO]);
|
||||
} catch (e) {
|
||||
localPageInfo = {};
|
||||
}
|
||||
const pi = conf.pageInfo || defaults; // The page info object to return
|
||||
pi.title = localPageInfo.title || conf.pageInfo.title || defaults.title;
|
||||
pi.description = localPageInfo.description || conf.pageInfo.description || defaults.description;
|
||||
pi.navLinks = localPageInfo.navLinks || conf.pageInfo.navLinks || defaults.navLinks;
|
||||
pi.footerText = localPageInfo.footerText || conf.pageInfo.footerText || defaults.footerText;
|
||||
return pi;
|
||||
})();
|
||||
|
||||
export const sections = (() => {
|
||||
// If the user has stored sections in local storage, return those
|
||||
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
|
||||
if (localSections) {
|
||||
try {
|
||||
const json = JSON.parse(localSections);
|
||||
if (json.length >= 1) return json;
|
||||
} catch (e) {
|
||||
// The data in local storage has been malformed, will return conf.sections instead
|
||||
}
|
||||
}
|
||||
// If the function hasn't yet returned, then return the config file sections
|
||||
return conf.sections;
|
||||
})();
|
||||
|
||||
export const config = (() => {
|
||||
const result = {
|
||||
appConfig,
|
||||
pageInfo,
|
||||
sections,
|
||||
};
|
||||
return result;
|
||||
})();
|
@ -95,6 +95,11 @@
|
||||
"default": false,
|
||||
"description": "Display a loading screen when the app is launched"
|
||||
},
|
||||
"statusCheck": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Displays an online/ offline status for each of your services"
|
||||
},
|
||||
"auth": {
|
||||
"type": "array",
|
||||
"description": "Usernames and hashed credentials for frontend authentication",
|
||||
@ -256,6 +261,11 @@
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"description": "Provider name, e.g. Microsoft"
|
||||
},
|
||||
"statusCheck": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether or not to display online/ offline status for this service. Will override appConfig.statusCheck"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user