diff --git a/src/App.vue b/src/App.vue index 2598f68a..83c7a959 100644 --- a/src/App.vue +++ b/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; diff --git a/src/components/LinkItems/Item.vue b/src/components/LinkItems/Item.vue index 5a89a00f..53cb47fc 100644 --- a/src/components/LinkItems/Item.vue +++ b/src/components/LinkItems/Item.vue @@ -20,12 +20,20 @@ + @@ -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 } diff --git a/src/components/LinkItems/ItemGroup.vue b/src/components/LinkItems/ItemGroup.vue index ef3dba8c..a375d7d6 100644 --- a/src/components/LinkItems/ItemGroup.vue +++ b/src/components/LinkItems/ItemGroup.vue @@ -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; + }, }, }; diff --git a/src/components/LinkItems/StatusIndicator.vue b/src/components/LinkItems/StatusIndicator.vue new file mode 100644 index 00000000..6b1864dd --- /dev/null +++ b/src/components/LinkItems/StatusIndicator.vue @@ -0,0 +1,122 @@ + + + + + + + diff --git a/src/main.js b/src/main.js index 90c589d6..eea75224 100644 --- a/src/main.js +++ b/src/main.js @@ -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'); diff --git a/src/router.js b/src/router.js index 968938f4..b32aa255 100644 --- a/src/router.js +++ b/src/router.js @@ -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: '/' }); diff --git a/src/utils/ConfigAccumalator.js b/src/utils/ConfigAccumalator.js new file mode 100644 index 00000000..2ecec4ac --- /dev/null +++ b/src/utils/ConfigAccumalator.js @@ -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; +})(); diff --git a/src/utils/ConfigSchema.json b/src/utils/ConfigSchema.json index 748b649d..7c8f5035 100644 --- a/src/utils/ConfigSchema.json +++ b/src/utils/ConfigSchema.json @@ -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" } } }