Merge pull request #34 from Lissy93/feature_status-indicators

Feature: Status Indicators (optional feature)
This commit is contained in:
Alicia Sykes 2021-06-14 20:59:09 +01:00 committed by GitHub
commit f03b6c44a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 455 additions and 104 deletions

View File

@ -19,6 +19,7 @@
- Customizable layout options, and item sizes
- Quickly preview a website, by holding down the Alt key while clicking, to open it in a resizable pop-up modal
- Many options for icons, including full Font-Awesome support and the ability to auto-fetch icon from URLs favicon
- Option to show service status for each of your apps / links, for basic availability and uptime monitoring
- Additional info for each item visible on hover (including opening method icon and description as a tooltip)
- Option for full-screen background image, custom nav-bar links, and custom footer text
- User preferences stored in local storage and applied on load
@ -46,7 +47,7 @@
## Getting Started 🛫
> For full setup instructions, see: [**Getting Started**](./docs/getting-started.md)
> For full setup instructions, see: [**Deployment**](./docs/deployment.md)
#### Deploying from Docker Hub 🐳
@ -104,7 +105,7 @@ You may find these [example config](https://gist.github.com/Lissy93/000f712a5ce9
## Theming 🎨
> For full configuration documentation, see: [**Theming**](./docs/theming.md)
> For full theming documentation, see: [**Theming**](./docs/theming.md)
<p align="center">
<a href="https://i.ibb.co/BVSHV1v/dashy-themes-slideshow.gif">
@ -122,7 +123,7 @@ You can also apply custom CSS overrides directly through the UI (Under Config me
## Cloud Backup & Sync ☁
> For full documentation, see: [**Cloud Backup & Sync**](./docs/backup-restore.md)
> For full backup documentation, see: [**Cloud Backup & Sync**](./docs/backup-restore.md)
Dashy has an **optional** built-in feature for securely backing up your config to a hosted cloud service, and then restoring it on another instance. This feature is totally optional, and if you do not enable it, then Dashy will not make any external network requests.
@ -136,7 +137,7 @@ All data is encrypted before being sent to the backend. In Dashy, this is done i
## Authentication 💂
> For full development documentation, see: [**Authentication**](./docs/authentication.md)
> For full authentication documentation, see: [**Authentication**](./docs/authentication.md)
Dashy has a built-in login feature, which can be used for basic access control. To enable this feature, add an `auth` attribute under `appConfig`, containing an array of users, each with a username, SHA-256 hashed password and optional user type.
@ -146,7 +147,17 @@ appConfig:
- user: alicia
hash: 4D1E58C90B3B94BCAD9848ECCACD6D2A8C9FBC5CA913304BBA5CDEAB36FEEFA3
```
At present, access control is handles on the frontend, and therefore in security-critical applications, it is recommended to use VPN access for authentication.
At present, access control is handled on the frontend, and therefore in security-critical situations, it is recommended to use an alternate method for authentication, such as [Authelia](https://www.authelia.com/), a VPN or web server and firewall rules.
**[⬆️ Back to Top](#dashy)**
---
## Status Indicators 🚦
> For full monitoring documentation, see: [**Status Indicators**](./docs/status-indicators.md)
Dashy has an optional feature that can display a small icon ([like this](./docs/assets/status-check-demo.gif)) next to each of your running services, indicating it's current status. This is useful if you are using Dashy as your homelab's start page, as it gives you an overview of the health of each of your running services. By default, this feature is off, but you can enable it globally by setting `appConfig.statusCheck: true`, or enable/ disable it for an individual item, with `item[n].statusCheck`.
**[⬆️ Back to Top](#dashy)**
@ -210,7 +221,7 @@ For more general questions about any of the technologies used, [StackOverflow](h
## Documentation 📘
- [Getting Started](/docs/getting-started.md)
- [Getting Started](/docs/deployment.md)
- [Configuring](/docs/configuring.md)
- [Developing](/docs/developing.md)
- [Contributing](/docs/contributing.md)

View File

@ -18,7 +18,7 @@ services:
# - GID=1000
restart: unless-stopped
healthcheck:
test: ['CMD', 'node', '/app/bin/healthcheck']
test: ['CMD', 'node', '/app/services/healthcheck']
interval: 1m30s
timeout: 10s
retries: 3

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -42,6 +42,7 @@ Since all authentication is happening entirely on the client-side, it is vulnera
Addressing this is on the todo list, and there are two potential solutions:
1. Encrypt all site data against the users password, so that an attacker can not physically access any data without the correct decryption key
2. Use a backend service to handle authentication, and do not return user data from the server until the correct credentials are provided. However, this would require either Dashy to be run using it's Node.js server, or the use of an external service
3. Implement authentication using a self-hosted identity management solution, such as [Keycloak for Vue](https://www.keycloak.org/securing-apps/vue)
**[⬆️ Back to Top](#authentication)**
@ -50,13 +51,27 @@ Addressing this is on the todo list, and there are two potential solutions:
## Alternative Authentication Methods
If you are hosting Dashy locally, and require remote access, it is recommend to configure a VPN connection into your local network. For instances running on the cloud, you have several other options:
- Authentication Server
- VPN
- IP-Based Access
- Web Server Authentication
- OAuth Services
- Password Protection (for cloud providers)
### Authentication Server
##### Authelia
[Authelia](https://www.authelia.com/) is an open-source full-featured authentication server, which can be self-hosted and either on bare metal, in a Docker container or in a Kubernetes cluster. It allows for fine-grained access control rules based on IP, path, users etc, and supports 2FA, simple password access or bypass policies for your domains.
- `git clone https://github.com/authelia/authelia.git`
- `cd authelia/examples/compose/lite`
- Modify the `users_database.yml` the default username and password is authelia
- Modify the `configuration.yml` and `docker-compose.yml` with your respective domains and secrets
- `docker-compose up -d`
For more information, see the [Authelia docs](https://www.authelia.com/docs/)
### VPN
The most secure method for accessing Dashy and other self-hosted services remotely is through a VPN connection, using something like [OpenVPN](https://openvpn.net/) or [WireGuard](https://www.wireguard.com/)
A catch-all solution to accessing services running from your home network remotely is to use a VPN. It means you do not need to worry about implementing complex authentication rules, or trusting the login implementation of individual applications. However it can be inconvenient to use on a day-to-day basis, and some public and corporate WiFi block VPN connections. Two popular VPN protocols are [OpenVPN](https://openvpn.net/) and [WireGuard](https://www.wireguard.com/)
### IP-Based Access
If you have a static IP or use a VPN to access your running services, then you can use conditional access to block access to Dashy from everyone except users of your pre-defined IP address. This feature is offered by most cloud providers, and supported by most web servers.
@ -154,7 +169,7 @@ Restart your web server for changes to take effect.
### OAuth Services
There are also authentication services, such as [Ory.sh](https://www.ory.sh/), [Okta](https://developer.okta.com/), [Auth0](https://auth0.com/), [Firebase](https://firebase.google.com/docs/auth/). Implementing one of these solutions would involve some changes to the [`Auth.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/Auth.js) file, but should be fairly straight forward.
## Static Site Hosting Providers
### Static Site Hosting Providers
If you are hosting Dashy on a cloud platform, you will probably find that it has built-in support for password protected access to web apps. For more info, see the relevant docs for your provider, for example: [Netlify Password Protection](https://docs.netlify.com/visitor-access/password-protection/), [Cloudflare Access](https://www.cloudflare.com/teams/access/), [AWS Cognito](https://aws.amazon.com/cognito/), [Azure Authentication](https://docs.microsoft.com/en-us/azure/app-service/scenario-secure-app-authentication-app-service) and [Vercel Password Protection](https://vercel.com/docs/platform/projects#password-protection).
**[⬆️ Back to Top](#authentication)**

View File

@ -1,6 +1,6 @@
# Getting Started
# Deployment
- [Deployment](#deployment)
- [Running the App](#running-the-app)
- [Deploy with Docker](#deploy-with-docker)
- [Deploy from Source](#deploy-from-source)
- [Deploy to Cloud Service](#deploy-to-cloud-service)
@ -17,7 +17,7 @@
- [NGINX](#nginx)
- [Apache](#apache)
## Deployment
## Running the App
### Deploy with Docker
@ -154,7 +154,7 @@ yarn build
surge ./dist
```
**[⬆️ Back to Top](#getting-started)**
**[⬆️ Back to Top](#deployment)**
---
@ -177,6 +177,7 @@ The following commands are defined in the [`package.json`](https://github.com/Li
- **`yarn health-check`** - Checks that the application is up and running on it's specified port, and outputs current status and response times. Useful for integrating into your monitoring service, if you need to maintain high system availability
- **`yarn build-watch`** - If you find yourself making frequent changes to your configuration, and do not want to have to keep manually rebuilding, then this option is for you. It will watch for changes to any files within the projects root, and then trigger a rebuild. Note that if you are developing new features, then `yarn dev` would be more appropriate, as it's significantly faster at recompiling (under 1 second), and has hot reloading, linting and testing integrated
- **`yarn build-and-start`** - Builds the app, runs checks and starts the production server. Commands are run in parallel, and so is faster than running them in independently
- **`yarn pm2-start`** - Starts the Node server using [PM2](https://pm2.keymetrics.io/), a process manager for Node.js applications, that helps them stay alive. PM2 has some built-in basic monitoring features, and an optional [management solution](https://pm2.io/). If you are running the app on bare metal, it is recommended to use this start command
### Healthchecks
@ -192,7 +193,7 @@ You can check the resource usage for your running Docker containers with `docker
You can also view logs, resource usage and other info as well as manage your Docker workflow in third-party Docker management apps. For example [Portainer](https://github.com/portainer/portainer) an all-in-one management web UI for Docker and Kubernetes, or [LazyDocker](https://github.com/jesseduffield/lazydocker) a terminal UI for Docker container management and monitoring.
**[⬆️ Back to Top](#getting-started)**
**[⬆️ Back to Top](#deployment)**
---
## Updating
@ -230,8 +231,7 @@ For more information, see the [Watchtower Docs](https://containrrr.dev/watchtowe
4. Re-build: `yarn build`
5. Start: `yarn start`
**[⬆️ Back to Top](#getting-started)**
**[⬆️ Back to Top](#deployment)**
---
@ -241,8 +241,9 @@ _The following section only applies if you are not using Docker, and would like
Dashy ships with a pre-configured Node.js server, in [`server.js`](https://github.com/Lissy93/dashy/blob/master/server.js) which serves up the contents of the `./dist` directory on a given port. You can start the server by running `node server`. Note that the app must have been build (run `yarn build`), and you need [Node.js](https://nodejs.org) installed.
However, since Dashy is just a static web application, it can be served with whatever server you like. The following section outlines how you can configure a web server.
If you wish to run Dashy from a sub page (e.g. `example.com/dashy`), then just set the `BASE_URL` environmental variable to that page name (in this example, `/dashy`), before building the app, and the path to all assets will then resolve to the new path, instead of `./`.
However, since Dashy is just a static web application, it can be served with whatever server you like. The following section outlines how you can configure a web server.
### NGINX
Create a new file in `/etc/nginx/sites-enabled/dashy`
@ -299,13 +300,13 @@ Then restart Apache, with `sudo systemctl restart apache2`
8. If you need to change the port, click 'Add environmental variable', give it the name 'PORT', choose a port number and press 'Save'.
9. Dashy should now be running at your selected path an on a given port
**[⬆️ Back to Top](#getting-started)**
**[⬆️ Back to Top](#deployment)**
---
## Authentication
Dashy has built-in client-side authentication, but for security-critical situations, it is recommend to either use a VPN for access, or implement your own authentication using your cloud provider, web server or firewall rules. For more info, see **[Authentication Docs](/docs/authentication.md)**.
Dashy has built-in authentication and login functionality. However, since this is handled on the client-side, if you are using Dashy in security-critical situations, it is recommended to use an alternate method for authentication, such as [Authelia](https://www.authelia.com/), a VPN or web server and firewall rules. For more info, see **[Authentication Docs](/docs/authentication.md)**.
**[⬆️ Back to Top](#getting-started)**
**[⬆️ Back to Top](#deployment)**

View File

@ -47,6 +47,17 @@ Note:
- If you are using NPM, replace `yarn` with `npm run`
- If you are using Docker, precede each command with `docker exec -it [container-id]`. Container ID can be found by running `docker ps`
### Environmental Variables
- `PORT` - The port in which the application will run (defaults to `4000` for the Node.js server, and `80` within the Docker container)
- `VUE_APP_DOMAIN` - The URL where Dashy is going to be accessible from. This should include the protocol, hostname and (if not 80 or 443), then the port too, e.g. `https://localhost:3000`, `http://192.168.1.2:4002` or `https://dashy.mydomain.com`
All environmental variables are optional. Currently there are not many environmental variables used, as most of the user preferences are stored under `appConfig` in the `conf.yml` file.
If you do add new variables, ensure that there is always a fallback (define it in [`defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js)), so as to not cause breaking changes. Don't commit your `.env` file to git, but instead take a few moments to document what you've added under the appropriate section. Try and follow the concepts outlined in the [12 factor app](https://12factor.net/config), as these are good practices.
Any environmental variables used by the frontend are preceded with `VUE_APP_`. Vue will merge the contents of your `.env` file into the app in a similar way to the ['dotenv'](https://github.com/motdotla/dotenv) package, where any variables that you set on your system will always take preference over the contents of any `.env` file.
### Resources for Beginners
New to Web Development? Glad you're here! Dashy is a pretty simple app, so it should make a good candidate for your first PR. Presuming that you already have a basic knowledge of JavaScript, the following articles should point you in the right direction for getting up to speed with the technologies used in this project:
- [Introduction to Vue.js](https://v3.vuejs.org/guide/introduction.html)
@ -79,7 +90,6 @@ The most significant things to note are:
For the full styleguide, see: [github.com/airbnb/javascript](https://github.com/airbnb/javascript)
### Frontend Components
All frontend code is located in the `./src` directory, which is split into 5 sub-folders:
@ -122,6 +132,9 @@ Running `yarn upgrade` will updated all dependencies based on the ranges specifi
#### Performance - Lighthouse
The easiest method of checking performance is to use Chromium's build in auditing tool, Lighthouse. To run the test, open Developer Tools (usually F12) --> Lighthouse and click on the 'Generate Report' button at the bottom.
#### Dependencies - BundlePhobia
[BundlePhobia](https://bundlephobia.com/) is a really useful app that lets you analyze the cost of adding any particular dependency to an application
### Directory Structure
#### Files in the Root: `./`

View File

@ -1,6 +1,6 @@
## Contents
- [Getting Started](/docs/getting-started.md)
- [Deployment](/docs/deployment.md)
- [Configuring](/docs/configuring.md)
- [Developing](/docs/developing.md)
- [Contributing](/docs/contributing.md)

46
docs/status-indicators.md Normal file
View File

@ -0,0 +1,46 @@
# Status Indicators
Dashy has an optional feature that can display a small icon next to each of your running services, indicating it's current status. This is useful if you are using Dashy as your homelab's start page, as it gives you an overview of the health of each of your running services.
<p align="center">
<img width="800" src="/docs/assets/status-check-demo.gif" />
</p>
## Enabling Status Indicators
By default, this feature is off. If you do not want this feature, just don't add the `statusCheck` to your conf.yml file, then no requests will be made.
To enable status checks, you can either turn it on for all items, by setting `appConfig.statusCheck: true`, like:
```yaml
appConfig:
statusCheck: true
```
Or you can enable/ disable it on a per-item basis, with the `item[n].statusCheck` attribute
```yaml
sections:
- name: Firewall
items:
- title: OPNsense
description: Firewall Central Management
icon: networking/opnsense.png
url: https://192.168.1.1
statusCheck: false
- title: MalTrail
description: Malicious traffic detection system
icon: networking/maltrail.png
url: http://192.168.1.1:8338
statusCheck: true
- title: Ntopng
description: Network traffic probe and network use monitor
icon: networking/ntop.png
url: http://192.168.1.1:3001
statusCheck: true
```
## How it Works
When Dashy is loaded, items with `statusCheck` enabled will make a request, to `https://[your-host-name]/ping?url=[address-or-servce]`, which in turn will ping that running service, and respond with a status code. Response time is calculated from the difference between start and end time of the request.
An indicator will display next to each item, and will be yellow while waiting for the response to return, green if request was successful, red if it failed, and grey if it was unable to make the request all together.
All requests are made straight from your server, there is no intermediary. So providing you are hosting Dashy yourself, and are checking the status of other self-hosted services, there shouldn't be any privacy concerns.

View File

@ -1,6 +1,6 @@
## User Guide
This article outlines how to use the application. If you are instead looking for deployment instructions, see [Getting Started](/docs/getting-started.md) and [Configuring](/docs/configuring.md)
This article outlines how to use the application. If you are instead looking for deployment instructions, see [Deployment](/docs/deployment.md) and [Configuring](/docs/configuring.md)
### Contents
- [Searching](#searching)
@ -66,7 +66,7 @@ You can also use Alt + Click or Alt + Enter, to open an item in a popup window.
### Sections and Items
The main content in Dashy is split into sections, which contain icons. You can have as many sections as you need, and each section can have an unlimited amount of icons. Visually, the grid layout works better when sections have a similar number of icons.
The main content in Dashy is defined as an array of sections, each of which contains an array of items. You can have as many sections as you need, and each section can have an unlimited amount of items. If you are using the grid layout, then it works better, visually if each of your sections have similar number of items.
Sections are collapsible, which is useful for those sections which contain less used applications, or are particularly long. The collapse state of a given section is remembered (stored in local storage), and applied on load.
@ -100,7 +100,7 @@ Sections also have several optional properties, which are specified under `secti
### Icons
Both sections and items can have an icon associated with them. There are several options for specifying icons. You can let the icon be automatically resolved and fetched from the items associated URL, by just setting the icon to `favicon`. You can use a font-awesome icon, by specifying it's name and category. Or you can pass in a URL, either to a locally hosted or remote image. For local images, you can put them in `./public/item-icons/` and then reference them just by the file name.
Both sections and items can have an icon associated with them. There are several options for specifying icons. You can let the icon be automatically resolved and fetched from the items associated URL, by setting it's value to `favicon`. You can use a font-awesome icon, by specifying it's name and category, e.g. `fas fa-rocket`. Or you can pass in a URL, either to a locally hosted or remote image. For local images, you can put them in `./public/item-icons/` and then reference them just by the file name.
**[⬆️ Back to Top](#user-guide)**

View File

@ -8,10 +8,11 @@
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint --fix",
"pm2-start": "npx pm2 start server.js",
"build-watch": "vue-cli-service build --watch",
"build-and-start": "npm-run-all --parallel build start",
"validate-config": "node src/utils/ConfigValidator",
"health-check": "node bin/healthcheck"
"health-check": "node services/healthcheck"
},
"dependencies": {
"ajv": "^8.5.0",

View File

@ -9,6 +9,8 @@ const os = require('os');
require('./src/utils/ConfigValidator');
const pingUrl = require('./services/ping');
const isDocker = !!process.env.IS_DOCKER;
/* Checks env var for port. If undefined, will use Port 80 for Docker, or 4000 for metal */
@ -64,8 +66,16 @@ const printWelcomeMessage = () => {
try {
connect()
.use(serveStatic(`${__dirname}/dist`))
.use(serveStatic(`${__dirname}/public`, { index: 'default.html' }))
.use(serveStatic(`${__dirname}/dist`)) /* Serves up the main built application to the root */
.use(serveStatic(`${__dirname}/public`, { index: 'default.html' })) /* During build, a custom page will be served */
.use('/ping', (req, res) => { /* This root returns the status of a given service - used for uptime monitoring */
try {
pingUrl(req.url, async (results) => {
await res.end(results);
});
// next();
} catch (e) { console.warn(`Error running ping check for ${req.url}\n`, e); }
})
.listen(port, () => {
try { printWelcomeMessage(); } catch (e) { console.log('Dashy is Starting...'); }
});

66
services/ping.js Normal file
View File

@ -0,0 +1,66 @@
/**
* This file contains the Node.js code, used for the optional status check feature
* It accepts a single url parameter, and will make an empty GET request to that
* endpoint, and then resolve the response status code, time taken, and short message
*/
const axios = require('axios').default;
/* Determines if successful from the HTTP response code */
const getResponseType = (code) => {
if (Number.isNaN(code)) return false;
const numericCode = parseInt(code, 10);
return (numericCode >= 200 && numericCode <= 302);
};
/* Makes human-readable response text for successful check */
const makeMessageText = (data) => `${data.successStatus ? '✅' : '⚠️'} `
+ `${data.serverName || 'Server'} responded with `
+ `${data.statusCode} - ${data.statusText}. `
+ `\nTook ${data.timeTaken} ms`;
/* Makes human-readable response text for failed check */
const makeErrorMessage = (data) => `❌ Service Unavailable: ${data.hostname || 'Server'} `
+ `resulted in ${data.code || 'a fatal error'} ${data.errno ? `(${data.errno})` : ''}`;
const makeErrorMessage2 = (data) => `❌ Service Error - `
+ `${data.status} - ${data.statusText}`;
/* Kicks of a HTTP request, then formats and renders results */
const makeRequest = (url, render) => {
const startTime = new Date();
axios.get(url)
.then((response) => {
const statusCode = response.status;
const { statusText } = response;
const successStatus = getResponseType(statusCode);
const serverName = response.request.socket.servername;
const timeTaken = (new Date() - startTime);
const results = {
statusCode, statusText, serverName, successStatus, timeTaken,
};
const messageText = makeMessageText(results);
results.message = messageText;
return results;
})
.catch((error) => {
render(JSON.stringify({
successStatus: false,
message: error.response ? makeErrorMessage2(error.response) : makeErrorMessage(error),
}));
}).then((results) => {
render(JSON.stringify(results));
});
};
/* Main function, will check if a URL present, and call function */
module.exports = (params, render) => {
if (!params || !params.includes('=')) {
render(JSON.stringify({
success: false,
message: '❌ Malformed URL',
}));
} else {
const url = params.split('=')[1];
makeRequest(url, render);
}
};

View File

@ -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;

View File

@ -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
}

View File

@ -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>

View 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>

View File

@ -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');

View File

@ -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: '/' });

View 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;
})();

View File

@ -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"
}
}
}

View File

@ -1,4 +1,5 @@
module.exports = {
publicPath: process.env.BASE_URL, // || './',
chainWebpack: config => {
config.module.rules.delete('svg');
},

View File

@ -2411,9 +2411,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
version "1.0.30001236"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001236.tgz#0a80de4cdf62e1770bb46a30d884fc8d633e3958"
integrity sha512-o0PRQSrSCGJKCPZcgMzl5fUaj5xHe8qA2m4QRvnyY4e1lITqoNkr7q/Oh1NcpGSy0Th97UZ35yoKcINPoq7YOQ==
version "1.0.30001237"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz#4b7783661515b8e7151fc6376cfd97f0e427b9e5"
integrity sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==
case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.4.0"
@ -3715,9 +3715,9 @@ eslint-plugin-standard@^4.0.0:
integrity sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ==
eslint-plugin-vue@^7.9.0:
version "7.11.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.11.0.tgz#c19b098899b7e3cd692beffbbe73611064ef1ea6"
integrity sha512-Qwo8wilqnOXnG9B5auEiTstyaHefyhHd5lEhhxemwXoWsAxIW2yppzuVudowC5n+qn1nMLNV9TANkTthBK7Waw==
version "7.11.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.11.1.tgz#77eb4b44032d5cca79f9af21d06991d8694a314a"
integrity sha512-lbw3vkEAGqYjqd1HpPFWHXtYaS8mILTJ5KOpJfRxO3Fo7o0wCf1zD7vSOasbm6nTA9xIgvZQ4VcyGIzQXxznHw==
dependencies:
eslint-utils "^2.1.0"
natural-compare "^1.4.0"
@ -7378,9 +7378,9 @@ regexpp@^2.0.1:
integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
regexpp@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
version "3.2.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
regexpu-core@^4.7.1:
version "4.7.1"