Rebased with master

This commit is contained in:
Alicia Sykes 2021-07-17 13:38:57 +01:00
commit a73e157992
121 changed files with 6563 additions and 839 deletions

View File

@ -0,0 +1,20 @@
---
name: "Add your Dashboard to the Showcase \U0001F5BC"
about: Share a screenshot of your dashboard to the Readme showcase!
title: "[SHOWCASE_REQUEST]"
labels: ''
assignees: ''
---
Please read the instructions here first:
https://github.com/Lissy93/dashy/blob/master/docs/showcase.md#submitting-your-dashboard
### Complete the Following
- **Title of Dashboard**:
- **Link to Screenshot**:
- **Would you like your name/ username included**: Yes/ No
- **Link to your Website/ Profile/ Twitter** (optional)
- **Description** (optional)
Either attach your screenshot here, or include a link to the CDN / image hosting service.

View File

@ -1,18 +1,23 @@
**Please check the type of change your PR introduces**:
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):
*Thank you for contributing to Dashy! So that your PR can be handled effectively, please populate the following fields (delete sections that are not applicable)*
**Issue Number** (if applicable):
**Category**:
> One of: Bugfix / Feature / Code style update / Refactoring Only / Build related changes / Documentation / Other (please specify)
**Briefly outline your changes**:
**Overview**
> Briefly outline your new changes...
**Issue Number** _(if applicable)_ #00
**New Vars** _(if applicable)_
> If you've added any new build scripts, environmental variables, config file options, dependency or devDependency, please outline here
**Screenshot** _(if applicable)_
> If you've introduced any significant UI changes, please include a screenshot
**Code Quality Checklist** _(Please complete)_
- [ ] All changes are backwards compatible
- [ ] All lint checks and tests are passing
- [ ] There are no (new) build warnings or errors
- [ ] _(If a new config option is added)_ Attribute is outlined in the schema and documented
- [ ] _(If a new dependency is added)_ Package is essential, and has been checked out for security or performance
**Before submitting, please ensure that**:
- [ ] Must be backwards compatible
- [ ] All lint checks and tests must pass
- [ ] If a new option in the the config file is added, it needs to be added into the schema, and documented in the configuring guide
- [ ] If a new dependency is required, it must be essential, and it must be thoroughly checked out for security or efficiency issues

17
LICENSE Normal file
View File

@ -0,0 +1,17 @@
Licensed under MIT X11. Copyright © 2021 Alicia Sykes <https://aliciasykes.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the “Software”), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWAREOR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

273
README.md
View File

@ -2,55 +2,73 @@
<h1 align="center">Dashy</h1>
<p align="center"><i>Dashy helps you organize your self-hosted services, by making them all accessible from a single place</i></p>
<p align="center">
<img src="https://app.codacy.com/project/badge/Grade/3be23a4a3a8a4689bd47745b201ecb74" /> <img src="https://img.shields.io/github/issues/lissy93/dashy?style=flat-square" /> <img src="https://img.shields.io/github/languages/code-size/lissy93/dashy?style=flat-square" /> <img src="https://img.shields.io/tokei/lines/github/lissy93/dashy?style=flat-square" />
</p>
<p align="center">
<img width="220" src="https://i.ibb.co/yhbt6CY/dashy.png" />
</p>
[![Awesome Self-Hosted](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/awesome-selfhosted/awesome-selfhosted#personal-dashboards)
![Docker Pulls](https://img.shields.io/docker/pulls/lissy93/dashy?logo=docker&style=flat-square)
![Stars](https://flat.badgen.net/github/stars/lissy93/dashy?icon=github)
![GitHub Status](https://flat.badgen.net/github/status/lissy93/dashy?icon=github)
![License MIT](https://img.shields.io/badge/License-MIT-09be48?style=flat-square&logo=opensourceinitiative)
![Current Version](https://img.shields.io/github/package-json/v/lissy93/dashy?style=flat-square&logo=azurepipelines&color=00af87)
[![Known Vulnerabilities](https://snyk.io/test/github/lissy93/dashy/badge.svg)](https://snyk.io/test/github/lissy93/dashy)
## Features 🌈
- Instant search by name, domain and tags - just start typing
- Full keyboard shortcuts for navigation, searching and launching
- Multiple color themes, with easy method for adding more
- 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
- Easy to customize every part of your dashboard, layout, icon sizes and colors etc
- Many options for icons, including full Font-Awesome support and the ability to auto-fetch icon from URLs favicon
- 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 settings stored in local storage and applied on load
- Option to show service status for each of your apps / links, for basic availability and uptime monitoring
- Multiple ways of opening apps, either in your browser, a pop-up modal or workspace view
- Option for full-screen background image, custom nav-bar links, html footer, title, and more
- Encrypted cloud backup and restore feature available
- Optional authentication, requiring user to log in
- Easy single-file YAML-based configuration
- Small bundle size, fully responsive UI and PWA makes the app easy to use on any device
- Plus lots more...
**Live Demos**: [Demo 1](https://dashy-demo-1.as93.net) ┆ [Demo 2](https://dashy-demo-2.as93.net) ┆ [Demo 3](https://dashy-demo-3.as93.net)
## Demo ⚡
**Spin up your own demo**: [![One-Click Deploy with PWD](https://img.shields.io/badge/Play--with--Docker-Deploy-2496ed?style=flat-square&logo=docker)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml)
> For more examples of Dashy in action, see: [**The Showcase**](./docs/showcase.md)
**Screenshots**
![Screenshots](https://i.ibb.co/r5T3MwM/dashy-screenshots.png)
#### Live Demos
[Demo 1](https://dashy-demo-1.as93.net) ┆ [Demo 2](https://dashy-demo-2.as93.net) ┆ [Demo 3](https://dashy-demo-3.as93.net)
**Recording**
#### Spin up your own Demo
- 1-Click Deploy: [![One-Click Deploy with PWD](https://img.shields.io/badge/Play--with--Docker-Deploy-2496ed?style=flat-square&logo=docker)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml)
- Or on your own machine: `docker run -p 8080:80 lissy93/dashy`
#### Recording
<p align="center">
<img width="800" src="https://i.ibb.co/L8YbNNc/dashy-demo2.gif" alt="Demo">
<img width="800" src="https://i.ibb.co/L8YbNNc/dashy-demo2.gif" alt="Demo" />
</p>
#### User Showcase
Are using Dashy? Want to share your dashboard here too - [Submit your Screenshots to the Showcase](./docs/showcase.md#submitting-your-dashboard)!
![Screenshots](https://i.ibb.co/r5T3MwM/dashy-screenshots.png)
**[⬆️ Back to Top](#dashy)**
---
## 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 🐳
You will need [Docker](https://docs.docker.com/get-docker/) installed on your system
```
docker run -p 8080:80 lissy93/dashy
```
Or
```docker
docker run -d \
-p 4000:80 \
@ -59,8 +77,8 @@ docker run -d \
--restart=always \
lissy93/dashy:latest
```
After making changes to your configuration file, you will need to run: `docker exec -it [container-id] yarn build` to rebuild. You can also run other commands, such as `yarn validate-config` this way too. Container ID can be found by running `docker ps`. Healthchecks are pre-configured to monitor the uptime and response times of Dashy, and the status of which can be seen in the container logs, e.g. `docker inspect --format "{{json .State.Health }}" [container-id]`.
You can also build the Docker container from source, by cloning the repo, cd'ing into it and running `docker build .` and `docker compose up`.
#### Deploying from Source 🚀
You will need both [git](https://git-scm.com/downloads) and the latest or LTS version of [Node.js](https://nodejs.org/) installed on your system
@ -71,15 +89,33 @@ You will need both [git](https://git-scm.com/downloads) and the latest or LTS ve
- Build: `yarn build`
- Run: `yarn start`
After making changes to your configuration file, you will need to run: `yarn build` to rebuild.
#### Deploy to the Cloud
Dashy supports 1-Click deployments on several popular cloud platforms (with more on the way!). To get started, just click a link below:
- [Deploy to Netlify](https://app.netlify.com/start/deploy?repository=https://github.com/lissy93/dashy)
- [Deploy to Heroku](https://heroku.com/deploy?template=https://github.com/Lissy93/dashy)
- [Deploy with Vercel](https://vercel.com/new/project?template=https://github.com/lissy93/dashy)
- [Deploy with PWD](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml)
Dashy supports 1-Click deployments on several popular cloud platforms. To spin up a new instance, just click a link below:
- [<img src="https://i.ibb.co/ZxtzrP3/netlify.png" width="18"/> Deploy to Netlify](https://app.netlify.com/start/deploy?repository=https://github.com/lissy93/dashy)
- [<img src="https://i.ibb.co/d2P1WZ7/heroku.png" width="18"/> Deploy to Heroku](https://heroku.com/deploy?template=https://github.com/Lissy93/dashy)
- [<img src="https://i.ibb.co/Ld2FZzb/vercel.png" width="18"/> Deploy to Vercel](https://vercel.com/new/project?template=https://github.com/lissy93/dashy)
- [<img src="https://i.ibb.co/xCHtzgh/render.png" width="18"/> Deploy to Render](https://render.com/deploy?repo=https://github.com/lissy93/dashy/tree/deploy_render)
- [<img src="https://i.ibb.co/HVWVYF7/docker.png" width="18"/> Deploy to PWD](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml)
#### Basic Commands
The following commands can be run on Dashy.
- `yarn build` - Builds the project for production, and outputs it into `./dist`
- `yarn start` - Starts a web server, and serves up the production site from `./dist`
- `yarn validate-config` - Parses and validates your `conf.yml` against Dashy's [schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.json)
- `yarn health-check` - Checks the health and status of Dashy's Node server
- `yarn pm2-start` - Starts the app using the [PM2](https://pm2.keymetrics.io/) process manager
- `yarn dev` - Starts the development server with hot reloading, linting, testing and verbose messaging
- `yarn lint` - Lints code to ensure it follows a consistent neat style
- `yarn test` - Runs tests, and outputs results
- `yarn install` - Install all dependencies
If you are using Docker, than precede each command with `docker exec -it [container-id]`, where container id can be found by running `docker ps`, e.g. `docker exec -it 92490c12baff yarn build`.
If you prefer [`NPM`](https://docs.npmjs.com), then just replace `yarn` with `npm run` in the following commands.
In Docker, [healthchecks](https://docs.docker.com/engine/reference/builder/#healthcheck) are pre-configured to monitor the uptime and response times of Dashy, and the status of which will show in your Docker monitoring app, or the `docker ps` command, or the container logs, using: `docker inspect --format "{{json .State.Health }}" [container-id]`.
**[⬆️ Back to Top](#dashy)**
@ -91,10 +127,12 @@ Dashy supports 1-Click deployments on several popular cloud platforms (with more
Dashy is configured with a single [YAML](https://yaml.org/) file, located at `./public/conf.yml` (or `./app/public/conf.yml` for Docker). Any other optional user-customizable assets are also located in the `./public/` directory, e.g. `favicon.ico`, `manifest.json`, `robots.txt` and `web-icons/*`. If you are using Docker, the easiest way to method is to mount a Docker volume (e.g. `-v /root/my-local-conf.yml:/app/public/conf.yml`)
In the production environment, the app needs to be rebuilt in order for changes to take effect. This can be done with `yarn build`, or `docker exec -it [container-id] yarn build` if you are using Docker (where container ID can be found by running `docker ps`).
In the production environment, the app needs to be rebuilt in order for changes to take effect. This should happen automatically, but can also be triggered by running `yarn build`, or `docker exec -it [container-id] yarn build` if you are using Docker (where container ID can be found by running `docker ps`).
You can check that your config matches Dashy's [schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.json) before deploying, by running `yarn validate-config.`
It is now possible also possible to update Dashy's config directly through the UI, and have changes written to disk. You can disable this feature by setting: `appConfig.allowConfigEdit: false`. If you are using users within Dashy, then you need to be logged in to a user of `type: admin` in order to modify the configuration globally. You can also trigger a rebuild of the app through the UI (Settings --> Rebuild).
You may find these [example config](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10) helpful for getting you started
**[⬆️ Back to Top](#dashy)**
@ -103,11 +141,11 @@ 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">
<img alt="Example Themes" src="/docs/assets/theme-slideshow.gif" width="400">
<img alt="Example Themes" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/theme-slideshow.gif" width="400" />
</a>
</p>
@ -119,9 +157,30 @@ You can also apply custom CSS overrides directly through the UI (Under Config me
---
## Icons 🧸
> For full iconography documentation, see: [**Icons**](./docs/icons.md)
Both sections and items can have an icon associated with them, and defined under the `icon` attribute. There are many options for icons, including Font Awesome support, automatic fetching from favicon, programmatically generated icons and direct local or remote URLs.
<p align="center">
<img width="400" src="https://i.ibb.co/GTVmZnc/dashy-example-icons.png" />
</p>
- **Favicon**: Set `icon: favicon` to fetch a services icon automatically from the URL of the corresponding application
- **Font-Awesome**: To use any font-awesome icon, specify the category, followed by the icon name, e.g. `fas fa-rocket` or `fab fa-monero`. You can also use Pro icons if you have a license key, just set it under `appConfig.fontAwesomeKey`
- **Generative**: Setting `icon: generative`, will generate a unique for a given service, based on it's URL or IP
- **Emoji**: Use an emoji as a tile icon, by putting the emoji's code as the icon attribute. Emojis can be specified either as emojis (`🚀`), unicode (`'U+1F680'`) or shortcode (`':rocket:'`).
- **URL**: You can also pass in a URL to an icon asset, hosted either locally or using any CDN service. E.g. `icon: https://i.ibb.co/710B3Yc/space-invader-x256.png`.
- **Local Image**: To use a local image, store it in `./public/item-icons/` (or create a volume in Docker: `-v /local/image/directory:/app/public/item-icons/`) , and reference it by name and extension - e.g. set `icon: image.png` to use `./public/item-icon/image.png`. You can also use sub-folders here if you have a lot of icons, to keep them organized.
**[⬆️ Back to Top](#dashy)**
---
## 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.
@ -133,6 +192,134 @@ All data is encrypted before being sent to the backend. In Dashy, this is done i
---
## Authentication 💂
> 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.
```yaml
appConfig:
auth:
- user: alicia
hash: 4D1E58C90B3B94BCAD9848ECCACD6D2A8C9FBC5CA913304BBA5CDEAB36FEEFA3
```
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.
<p align="center">
<img
title="Example login screen, using Vapourwave theme"
alt="Example login screen, using Vapourwave theme"
src="https://i.ibb.co/K52YL1g/dashy-login-form.png"
width="400"
/>
</p>
**[⬆️ 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 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. Hovering over the indicator will show additional information, including average response time and an error message for services which are down.
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`. You can also specify an time interval in seconds under `appConfig.statusCheckInterval`, which will determine how often to recheck services, if this value is `0`, then status is only checked on initial page load, this is default behavior.
<p align="center">
<img alt="Status Checks demo" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/status-check-demo.gif" width="600" />
</p>
**[⬆️ Back to Top](#dashy)**
---
## Opening Methods 🖱️
One of the primary purposes of Dashy is to make launching commonly used apps and services as quick as possible. To aid in this, there are several different options on how items can be opened. You can configure your preference by setting the `target` property of any item, to one of the following values:
- `sametab` - The app will be launched in the current tab
- `newtab` - The app will be launched in a new tab
- `modal` - Launch app in a resizable/ movable popup modal on the current page
- `workspace` - Changes to Workspace view, and launches app
Even if the target is not set (or is set to `sametab`), you can still launch any given app in an alternative method: Alt + Click will open the modal, and Ctrl + Click will open in a new tab. You can also right-click on any item to see all options (as seen in the screenshot below). This custom context menu can be disabled by setting `appConfig.disableContextMenu: true`.
<p align="center">
<img width="500" src="https://i.ibb.co/vmZdSRt/dashy-context-menu-2.png" />
</p>
The modal and workspace views work by rendering the target application in an iframe. For this to work, the HTTP response header [`X-Frame-Options`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) for a given application needs to be set to `ALLOW`. If you are getting a `Refused to Connect` error then this header is set to `DENY` (or `SAMEORIGIN` and it's on a different host).
Here's a quick demo of the workspace view:
<p align="center">
<img alt="Workspace view demo" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/workspace-demo.gif" width="600" />
</p>
**[⬆️ Back to Top](#dashy)**
---
## Config Editor ⚙️
From the Settings Menu in Dashy, you can download, backup, edit and rest your config. An interactive editor makes editing the config file easy, it will tell you if you've got any errors. After making your changes, you can either apply them locally, or export into your main config file. After saving to the config file to the disk, the app will need to be rebuilt. This will happen automatically, but may take a few minutes. You can also manually trigger a rebuild from the Settings Menu. A full list of available config options can be found [here](./docs/configuring.md). It's recommend to make a backup of your configuration, as you can then restore it into a new instance of Dashy, without having to set it up again. [json2yaml](https://www.json2yaml.com/) is very useful for converting between YAML to JSON and visa versa.
<p align="center">
<img alt="Workspace view demo" src="https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/config-editor-demo.gif" width="600" />
</p>
**[⬆️ Back to Top](#dashy)**
---
## Sections & Items 🗃️
Dashy is made up of a series of sections, each containing a series of items.
A section an be collapsed by clicking on it's name. This will cause only the title button to be visible until clicked, which is useful for particularly long sections, or those containing less-used apps. The collapse state for each section will be remembered for the next time you visit.
From the UI, you can also choose a layout, either `grid`, `horizontal` or `vertical`, as well as set the size for items, either `small`, `medium` or `large`, and of course set a theme using the dropdown. All settings specified here will be stored in your browsers local storage, and so won't persist across devices, if you require this then you must set these in the config file instead.
Within each section, you can set custom layout properties with under `displayData`. For example, you can make a given section double the width by making is span 2 columns with `cols: 2`, or specify how many rows it should span with `rows`. You can also set the layout for items within a given section here, for example, use `itemCountX` to define how many items will be on each row within the section. Sections can also have a custom color, specified as a hex code and defined using the `color` attribute. For full options for items, see the [`section.displayData` docs](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#sectiondisplaydata-optional)
Items also have some optional config attributes. As well as `title`, `description`, `URL` and `icon`, you can also specify a specific opening method (`target`), and configure status checks (`statusCheck: true/false`, `statusCheckUrl` and `statusCheckHeaders`), and modify appearance with `color` and `backgroundColor`. For full options for items, see the [`section.item` docs](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#sectionitem)
**[⬆️ Back to Top](#dashy)**
---
## Setting Dashboard Info 🌳
Page settings are defined under [`pageInfo`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfo). Here you can set things like title, sub-title, navigation links, footer text, etc
Custom links for the navigation menu are defined under [`pageInfo.navLinks`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfonavlinks-optional).
You can display either custom text or HTML in the footer, using the `pageInfo.footerText` attribute.
It's also possible to hide parts of the page that you do not need (e.g. navbar, footer, search, heading, etc). This is done using the [`appConfig.hideComponents`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#appconfighidecomponents-optional) attribute.
For example, a `pageInfo` section might look something like this:
```yaml
pageInfo:
title: Home Lab
description: Dashy
navLinks:
- title: Home
path: /
- title: Server Monitoring
path: https://server-start.local
- title: Start Page
path: https://start-page.local
footerText: 'Built with Dashy, by <a href="https://aliciasykes.com">Alicia Sykes</a>, 2021'
```
**[⬆️ Back to Top](#dashy)**
---
## Developing 🧱
> For full development documentation, see: [**Developing**](./docs/developing.md)
@ -155,7 +342,7 @@ If you are new to Vue.js or web development and want to learn more, [here are so
Pull requests are welcome, and would by much appreciated!
Some ideas for PRs include: bug fixes, improve the docs, add new themes, implement a new widget, add or improve the display options, improve or refactor the code, or implement a new feature.
Some ideas for PRs include: bug fixes, improve the docs, submit a screenshot of your dashboard to the showcase, add new themes, implement a new widget, add or improve the display options, improve or refactor the code, or implement a new feature.
Before you submit your pull request, please ensure the following:
- Must be backwards compatible
@ -164,6 +351,14 @@ Before you submit your pull request, please ensure the following:
- If a new dependency is required, it must be essential, and it must be thoroughly checked out for security or efficiency issues
- Your pull request will need to be up-to-date with master, and the PR template must be filled in
### Repo Status
![Open PRs](https://flat.badgen.net/github/open-prs/lissy93/dashy?icon=github)
![Total PRs](https://flat.badgen.net/github/prs/lissy93/dashy?icon=github)
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/lissy93/dashy?style=flat-square)
![Last Commit](https://flat.badgen.net/github/last-commit/lissy93/dashy?icon=github)
![Contributors](https://flat.badgen.net/github/contributors/lissy93/dashy?icon=github)
**[⬆️ Back to Top](#dashy)**
---
@ -179,6 +374,9 @@ If you've found a bug, or something that isn't working as you'd expect, please r
- [Ask a Question 🤷‍♀️](https://github.com/Lissy93/dashy/issues/new?assignees=Lissy93&labels=%F0%9F%A4%B7%E2%80%8D%E2%99%82%EF%B8%8F+Question&template=question------.md&title=%5BQUESTION%5D)
- [Share Feedback 🌈](https://github.com/Lissy93/dashy/issues/new?assignees=&labels=%F0%9F%8C%88+Feedback&template=share-feedback---.md&title=%5BFEEDBACK%5D)
[**Issue Status**](https://isitmaintained.com/project/lissy93/dashy) ![Resolution Time](http://isitmaintained.com/badge/resolution/lissy93/dashy.svg) ![Open Issues](http://isitmaintained.com/badge/open/lissy93/dashy.svg) ![Closed Issues](https://badgen.net/github/closed-issues/lissy93/dashy)
For more general questions about any of the technologies used, [StackOverflow](https://stackoverflow.com/questions/) may be more helpful first port of info
If you need to get in touch securely with the author (me, Alicia Sykes), drop me a message at:
@ -191,14 +389,18 @@ For more general questions about any of the technologies used, [StackOverflow](h
## Documentation 📘
- [Getting Started](/docs/getting-started.md)
- [Deployment](/docs/deployment.md)
- [Configuring](/docs/configuring.md)
- [Developing](/docs/developing.md)
- [Contributing](/docs/contributing.md)
- [User Guide](/docs/user-guide.md)
- [Troubleshooting](/docs/troubleshooting.md)
- [Backup & Restore](/docs/backup-restore.md)
- [Status Indicators](/docs/status-indicators.md)
- [Theming](/docs/theming.md)
- [Icons](/docs/icons.md)
- [Authentication](/docs/authentication.md)
- [Showcase](/docs/showcase.md)
**[⬆️ Back to Top](#dashy)**
@ -241,7 +443,8 @@ The 1-Click deploy demo uses [Play-with-Docker Labs](https://play-with-docker.co
### Alternatives 🙌
There are a few self-hosted web apps, that serve a similar purpose to Dashy. If you're looking for a dashboard, and Dashy doesn't meet your needs, I highly recommend you check these projects out! Including, but not limited to: [HomeDash2](https://lamarios.github.io/Homedash2), [Homer](https://github.com/bastienwirtz/homer) (`Apache License 2.0`), [Organizr](https://organizr.app/) (`GPL-3.0 License`) and [Heimdall](https://github.com/linuxserver/Heimdall) (`MIT License`)
There are a few self-hosted web apps, that serve a similar purpose to Dashy. If you're looking for a dashboard, and Dashy doesn't meet your needs, I highly recommend you check these projects out!
[HomeDash2](https://lamarios.github.io/Homedash2), [Homer](https://github.com/bastienwirtz/homer) (`Apache License 2.0`), [Organizr](https://organizr.app/) (`GPL-3.0 License`) and [Heimdall](https://github.com/linuxserver/Heimdall) (`MIT License`)
**[⬆️ Back to Top](#dashy)**
@ -268,6 +471,14 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWAREOR THE
OR OTHER DEALINGS IN THE SOFTWARE.
```
**TDLR;** _You can do whatever you like with Dashy: use it in private or commercial settings,_
_redistribute and modify it. But you must display this license and credit the author._
_There is no warranty that this app will work as expected, and the author cannot be held_
_liable for anything that goes wrong._
For more info, see TLDR Legal's [Explanation of MIT](https://tldrlegal.com/license/mit-license)
![Octocat](https://github.githubassets.com/images/icons/emoji/octocat.png?v8)
**[⬆️ Back to Top](#dashy)**
---

View File

@ -12,14 +12,18 @@ services:
# - /root/my-config.yml:/app/public/conf.yml
ports:
- 4000:80
# Set any environmental variables
environment:
- NODE_ENV=production
# Specify your user ID and group ID. You can find this by running `id -u` and `id -g`
# environment:
# - UID=1000
# - GID=1000
# Specify restart policy
restart: unless-stopped
# Configure healthchecks
healthcheck:
test: ['CMD', 'node', '/app/bin/healthcheck']
test: ['CMD', 'node', '/app/services/healthcheck']
interval: 1m30s
timeout: 10s
retries: 3
start_period: 40s
start_period: 40s

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 741 KiB

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

177
docs/authentication.md Normal file
View File

@ -0,0 +1,177 @@
# Authentication
- [Built-In Login Feature](#authentication)
- [Setting Up Authentication](#setting-up-authentication)
- [Hash Password](#hash-password)
- [Logging In and Out](#logging-in-and-out)
- [Security](#security)
- [Alternative Authentication Methods](#alternative-authentication-methods)
- [VPN](#vpn)
- [IP-Based Access](#ip-based-access)
- [Web Server Authentication](#web-server-authentication)
- [OAuth Services](#oauth-services)
- [Auth on Cloud Hosting Services](#static-site-hosting-providers)
Dashy has a basic login page included, and frontend authentication. You can enable this by adding users to the `auth` section under `appConfig` in your `conf.yml`. If this section is not specified, then no authentication will be required to access the app, and it the homepage will resolve to your dashboard.
## Setting Up Authentication
The `auth` property takes an array of users. Each user needs to include a username, hash and optional user type (`admin` or `normal`). The hash property is a [SHA-256 Hash](https://en.wikipedia.org/wiki/SHA-2) of your desired password.
For example:
```yaml
appConfig:
auth:
- user: alicia
hash: 4D1E58C90B3B94BCAD9848ECCACD6D2A8C9FBC5CA913304BBA5CDEAB36FEEFA3
type: admin
- user: edward
hash: 5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
type: admin
```
## Hash Password
Dashy uses [SHA-256 Hash](https://en.wikipedia.org/wiki/Sha-256), a 64-character string, which you can generate using an online tool, such as [this one](https://passwordsgenerator.net/sha256-hash-generator/) or [CyberChef](https://gchq.github.io/CyberChef/) (which can be self-hosted/ ran locally).
A hash is a one-way cryptographic function, meaning that it is easy to generate a hash for a given password, but very hard to determine the original password for a given hash. This means, that so long as your password is long, strong and unique, it is safe to store it's hash in the clear. Having said that, you should never reuse passwords, hashes can be cracked by iterating over known password lists, generating a hash of each.
## Logging In and Out
Once authentication is enabled, so long as there is no valid token in cookie storage, the application will redirect the user to the login page. When the user enters credentials in the login page, they will be checked, and if valid, then a token will be generated, and they can be redirected to the home page. If credentials are invalid, then an error message will be shown, and they will remain on the login page. Once in the application, to log out the user can click the logout button (in the top-right), which will clear cookie storage, causing them to be redirected back to the login page.
## Security
Since all authentication is happening entirely on the client-side, it is vulnerable to manipulation by an adversary. An attacker could look at the source code, find the function used generate the auth token, then decode the minified JavaScript to find the hash, and manually generate a token using it, then just insert that value as a cookie using the console, and become a logged in user. Therefore, if you need secure authentication for your app, it is strongly recommended to implement this using your web server, or use a VPN to control access to Dashy. The purpose of the login page is merely to prevent immediate unauthorized access to your homepage.
Addressing this is on the todo list, and there are several 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 configuration, with no user data returned 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)**
---
## Alternative Authentication Methods
If you are self-hosting Dashy, and require secure authentication to prevent unauthorized access, you have several options:
- [Authentication Server](#authentication-server) - Put Dashy behind a self-hosted auth server
- [VPN](#vpn) - Use a VPN to tunnel into the network where Dashy is running
- [IP-Based Access](#ip-based-access) - Disallow access from all IP addresses, except your own
- [Web Server Authentication](#web-server-authentication) - Enable user control within your web server or proxy
- [OAuth Services](#oauth-services) - Implement a user management system using a cloud provider
- [Password Protection (for cloud providers)](#static-site-hosting-providers) - Enable password-protection on your site
### 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
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.
##### Apache
In Apache, this is configured in your `.htaccess` file in Dashy's root folder, and should look something like:
```
Order Deny,Allow
Deny from all
Allow from [your-ip]
```
##### NGINX
In NGINX you can specify [control access](https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-http/) rules for a given site in your `nginx.conf` or hosts file. For example:
```
server {
listen 80;
server_name www.dashy.example.com;
location / {
root /path/to/dashy/;
passenger_enabled on;
allow [your-ip];
deny all;
}
}
```
##### Caddy
In Caddy, [Request Matchers](https://caddyserver.com/docs/caddyfile/matchers) can be used to filter requests
```
dashy.site {
@public_networks not remote_ip [your-ip]
respond @public_networks "Access denied" 403
}
```
### Web Server Authentication
Most web servers make password protecting certain apps very easy. Note that you should also set up HTTPS and have a valid certificate in order for this to be secure.
##### Apache
First crate a `.htaccess` file in Dashy's route directory. Specify the auth type and path to where you want to store the password file (usually the same folder). For example:
```
AuthType Basic
AuthName "Please Sign into Dashy"
AuthUserFile /path/dashy/.htpasswd
require valid-user
```
Then create a `.htpasswd` file in the same directory. List users and their hashed passwords here, with one user on each line, and a colon between username and password (e.g. `[username]:[hashed-password]`). You will need to generate an MD5 hash of your desired password, this can be done with an [online tool](https://www.web2generators.com/apache-tools/htpasswd-generator). Your file will look something like:
```
alicia:$apr1$jv0spemw$RzOX5/GgY69JMkgV6u16l0
```
##### NGINX
NGINX has an [authentication module](https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html) which can be used to add passwords to given sites, and is fairly simple to set up. Similar to above, you will need to create a `.htpasswd` file. Then just enable auth and specify the path to that file, for example:
```
location / {
auth_basic "closed site";
auth_basic_user_file conf/htpasswd;
}
```
##### Caddy
Caddy has a [basic-auth](https://caddyserver.com/docs/caddyfile/directives/basicauth) directive, where you specify a username and hash. The password hash needs to be base-64 encoded, the [`caddy hash-password`](https://caddyserver.com/docs/command-line#caddy-hash-password) command can help with this. For example:
```
basicauth /secret/* {
alicia JDJhJDEwJEVCNmdaNEg2Ti5iejRMYkF3MFZhZ3VtV3E1SzBWZEZ5Q3VWc0tzOEJwZE9TaFlZdEVkZDhX
}
```
For more info about implementing a single sign on for all your apps with Caddy, see [this tutorial](https://joshstrange.com/securing-your-self-hosted-apps-with-single-signon/)
##### Lighttpd
You can use the [mod_auth](https://doc.lighttpd.net/lighttpd2/mod_auth.html) module to secure your site with Lighttpd. Like with Apache, you need to first create a password file listing your usersnames and hashed passwords, but in Lighttpd, it's usually called `.lighttpdpassword`.
Then in your `lighttpd.conf` file (usually in the `/etc/lighttpd/` directory), load in the mod_auth module, and configure it's directives. For example:
```
server.modules += ( "mod_auth" )
auth.debug = 2
auth.backend = "plain"
auth.backend.plain.userfile = "/home/lighttpd/.lighttpdpassword"
$HTTP["host"] == "dashy.my-domain.net" {
server.document-root = "/home/lighttpd/dashy.my-domain.net/http"
server.errorlog = "/var/log/lighttpd/dashy.my-domain.net/error.log"
accesslog.filename = "/var/log/lighttpd/dashy.my-domain.net/access.log"
auth.require = (
"/docs/" => (
"method" => "basic",
"realm" => "Password protected area",
"require" => "user=alicia"
)
)
}
```
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
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

@ -49,6 +49,20 @@ Maximum of 24mb of storage per user. Please do not repeatedly hit the endpoint,
- Add your `zone_id` (found in the Overview tab of your desired domain on Cloudflare)
- Add your `route`, which should be a domain or host, supporting a wildcard
```toml
name = "dashy-worker"
type = "javascript"
workers_dev = true
route = "example.com/*"
zone_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
account_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
kv_namespaces = [
{ binding = "DASHY_CLOUD_BACKUP", id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
]
```
#### Complete `index.js`
- Write code to handle your requests, and interact with any other data sources in this file
- Generally, this is done within an event listener for 'fetch', and returns a promise
@ -66,7 +80,7 @@ async function handleRequest(request) {
}
```
- For the code used for Dashy's cloud service, see [here](https://notes.aliciasykes.com/p/j2F1deljv1)
- For the code used for Dashy's cloud service, see [here](https://gist.github.com/Lissy93/d19b43d50f30e02fa25f349cf5cb5ed8#file-index-js)
#### Commands

View File

@ -1,19 +1,26 @@
## Configuring
# Configuring
All app configuration is specified in [`/public/conf.yml`](https://github.com/Lissy93/dashy/blob/master/public/conf.yml) which is in [YAML Format](https://yaml.org/) format.
If you're new to YAML, it's pretty straight-forward. The format is exactly the same as that of JSON, but instead of using curley braces, structure is denoted using whitespace. This [quick guide](https://linuxhandbook.com/yaml-basics/) should get you up to speed in a few minutes, for more advanced topics take a look at this [Wikipedia article](https://en.wikipedia.org/wiki/YAML) and for some practicle examples, the [Azure pipelines schema](https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema%2Cparameter-schema) may be useful.
You may find it helpful to look at some sample config files to get you started, a collection of which can be found [here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10).
There's a couple of things to remember, before getting started:
- After modifying your config, you will need to run `yarn build` to recompile the application
Tips:
- You may find it helpful to look at some sample config files to get you started, a collection of which can be found [here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10)
- You can check that your config file fits the schema, by running `yarn validate-config`
- Any changes made locally through the UI need to be exported into this file, in order for them to persist across devices
- After modifying your config, the app needs to be recompiled, by running `yarn build` - this happens automatically whilst the app is running
- It is recommended to make and keep a backup of your config file. You can download your current config through the UI either from the Config menu, or using the `/download` endpoint.
- All fields are optional, unless otherwise stated.
All fields are optional, unless otherwise stated.
### About YAML
If you're new to YAML, it's pretty straight-forward. The format is exactly the same as that of JSON, but instead of using curly braces, structure is denoted using whitespace. This [quick guide](https://linuxhandbook.com/yaml-basics/) should get you up to speed in a few minutes, for more advanced topics take a look at this [Wikipedia article](https://en.wikipedia.org/wiki/YAML) and for some practicle examples, the [Azure pipelines schema](https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema%2Cparameter-schema) may be useful.
#### Top-Level Fields
### Config Saving Methods
When updating the config through the JSON editor in the UI, you have two save options: **Local** or **Write to Disk**.
- Changes saved locally will only be applied to the current user through the browser, and will not apply to other instances - you either need to use the cloud sync feature, or manually update the conf.yml file.
- On the other-hand, if you choose to write changes to disk, then your main `conf.yml` file will be updated, and changes will be applied to all users, and visible across all devices. For this functionality to work, you must be running Dashy with using the Docker container, or the Node server. A backup of your current configuration will also be saved in the same directory.
### Preventing Changes being Written to Disk
To disallow any changes from being written to disk via the UI config editor, set `appConfig.allowConfigEdit: false`. If you are using users, and have setup `auth` within Dashy, then only users with `type: admin` will be able to write config changes to disk.
### Top-Level Fields
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
@ -23,7 +30,7 @@ All fields are optional, unless otherwise stated.
**[⬆️ Back to Top](#configuring)**
#### `PageInfo`
### `PageInfo`
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
@ -34,7 +41,7 @@ All fields are optional, unless otherwise stated.
**[⬆️ Back to Top](#configuring)**
#### `pageInfo.navLinks` _(optional)_
### `pageInfo.navLinks` _(optional)_
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
@ -43,22 +50,54 @@ All fields are optional, unless otherwise stated.
**[⬆️ Back to Top](#configuring)**
#### `appConfig` _(optional)_
### `appConfig` _(optional)_
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`statusCheck`** | `boolean` | _Optional_ | When set to `true`, Dashy will ping each of your services and display their status as a dot next to each item. This can be overridden by setting `statusCheck` under each item. Defaults to `false`
**`statusCheckInterval`** | `boolean` | _Optional_ | The number of seconds between checks. If set to `0` then service will only be checked on initial page load, which is usually the desired functionality. If value is less than `10` you may experience a hit in performance. Defaults to `0`
**`backgroundImg`** | `string` | _Optional_ | Path to an optional full-screen app background image. This can be either remote (http) or local (/). Note that this will slow down initial load
**`enableFontAwesome`** | `boolean` | _Optional_ | Where `true` is enabled, if left blank font-awesome will be enabled only if required by 1 or more icons
**`fontAwesomeKey`** | `string` | _Optional_ | If you have a font-awesome key, then you can use it here and make use of premium icons. It is a 10-digit alpha-numeric string from you're FA kit URL (e.g. `13014ae648`)
**`faviconApi`** | `string` | _Optional_ | Only applicable if you are using favicons for item icons. Specifies which service to use to resolve favicons. Set to `local` to do this locally, without using an API. Services running locally will use this option always. Available options are: `local`, `faviconkit`, `google`, `clearbit`, `webmasterapi` and `allesedv`. Defaults to `faviconkit`. See [Icons](/docs/icons.md#favicons) for more info
**`auth`** | `array` | _Optional_ | An array of objects containing usernames and hashed passwords. If this is not provided, then authentication will be off by default, and you will not need any credentials to access the app. Note authentication is done on the client side, and so if your instance of Dashy is exposed to the internet, it is recommend to configure your web server to handle this. See [`auth`](#appconfigauth-optional)
**`layout`** | `string` | _Optional_ | App layout, either `horizontal`, `vertical`, `auto` or `sidebar`. Defaults to `auto`. This specifies the layout and direction of how sections are positioned on the home screen. This can also be modified from the UI.
**`iconSize`** | `string` | _Optional_ | The size of link items / icons. Can be either `small`, `medium,` or `large`. Defaults to `medium`. This can also be set directly from the UI.
**`theme`** | `string` | _Optional_ | The default theme for first load (you can change this later from the UI)
**`cssThemes`** | `string[]` | _Optional_ | An array of custom theme names which can be used in the theme switcher dropdown
**`externalStyleSheet`** | `string` or `string[]` | _Optional_ | Either a URL to an external stylesheet or an array or URLs, which can be applied as themes within the UI
**`customCss`** | `string` | _Optional_ | Raw CSS that will be applied to the page. This can also be set from the UI. Please minify it first.
**`showSplashScreen`** | `boolean` | _Optional_ | Should display a splash screen while the app is loading. Defaults to false, except on first load
**`hideComponents`** | `object` | _Optional_ | A list of key page components (header, footer, search, settings, etc) that are present by default, but can be removed using this option. See [`appConfig.hideComponents`](#appconfighideComponents-optional)
**`allowConfigEdit`** | `boolean` | _Optional_ | Should prevent / allow the user to write configuration changes to the conf.yml from the UI. When set to `false`, the user can only apply changes locally using the config editor within the app, whereas if set to `true` then changes can be written to disk directly through the UI. Defaults to `true`. Note that if authentication is enabled, the user must be of type `admin` in order to apply changes globally.
**`disableServiceWorker`** | `boolean` | _Optional_ | Service workers cache web applications to improve load times and offer basic offline functionality, and are enabled by default in Dashy. The service worker can sometimes cause older content to be cached, requiring the app to be hard-refreshed. If you do not want SW functionality, or are having issues with caching, set this property to `true` to disable all service workers.
**`disableContextMenu`** | `boolean` | _Optional_ | If set to `true`, the custom right-click context menu will be disabled. Defaults to `false`.
**[⬆️ Back to Top](#configuring)**
#### `section`
### `appConfig.auth` _(optional)_
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`user`** | `string` | Required | Username to log in with
**`hash`** | `string` | Required | A SHA-256 hashed password
**`type`** | `string` | _Optional_ | The user type, either admin or normal
**[⬆️ Back to Top](#configuring)**
### `appConfig.hideComponents` _(optional)_
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`hideHeading`** | `boolean` | _Optional_ | If set to `true`, the page title & sub-title will not be visible. Defaults to `false`
**`hideNav`** | `boolean` | _Optional_ | If set to `true`, the navigation menu will not be visible. Defaults to `false`
**`hideSearch`** | `boolean` | _Optional_ | If set to `true`, the search bar will not be visible. Defaults to `false`
**`hideSettings`** | `boolean` | _Optional_ | If set to `true`, the settings menu will not be visible. Defaults to `false`
**`hideFooter`** | `boolean` | _Optional_ | If set to `true`, the footer will not be visible. Defaults to `false`
**`hideSplashScreen`** | `boolean` | _Optional_ | If set to `true`, splash screen will not be visible while the app loads. Defaults to `true` (except on first load, when the loading screen is always shown)
**[⬆️ Back to Top](#configuring)**
### `section`
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
@ -69,7 +108,7 @@ All fields are optional, unless otherwise stated.
**[⬆️ Back to Top](#configuring)**
#### `section.item`
### `section.item`
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
@ -77,13 +116,16 @@ All fields are optional, unless otherwise stated.
**`description`** | `string` | _Optional_ | Additional info about an item, which is shown in the tooltip on hover, or visible on large tiles
**`url`** | `string` | Required | The URL / location of web address for when the item is clicked
**`icon`** | `string` | _Optional_ | The icon for a given item. Can be a font-awesome icon, favicon, remote URL or local URL. See [`item.icon`](#sectionicon-and-sectionitemicon)
**`target`** | `string` | _Optional_ | The opening method for when the item is clicked, either `newtab`, `sametab` or `iframe`. Where `newtab` will open the link in a new tab, `sametab` will open it in the current tab, and `iframe` will open a pop-up modal with the content displayed within that iframe. Note that for the iframe to load, you must have set the CORS headers to either allow `*` ot allow the domain that you are hosting Dashy on, for some websites and self-hosted services, this is already set.
**`target`** | `string` | _Optional_ | The opening method for when the item is clicked, either `newtab`, `sametab`, `modal` or `workspace`. Where `newtab` will open the link in a new tab, `sametab` will open it in the current tab, and `modal` will open a pop-up modal with the content displayed within that iframe. Note that for the iframe to load, you must have set the CORS headers to either allow `*` ot allow the domain that you are hosting Dashy on, for some websites and self-hosted services, this is already set.
**`statusCheck`** | `boolean` | _Optional_ | When set to `true`, Dashy will ping the URL associated with the current service, and display its status as a dot next to the item. The value here will override `appConfig.statusCheck` so you can turn off or on checks for a given service. Defaults to `appConfig.statusCheck`, falls back to `false`
**`statusCheckUrl`** | `string` | _Optional_ | If you've enabled `statusCheck`, and want to use a different URL to what is defined under the item, then specify it here
**`statusCheckHeaders`** | `object` | _Optional_ | If you're endpoint requires any specific headers for the status checking, then define them here
**`color`** | `string` | _Optional_ | An optional color for the text and font-awesome icon to be displayed in. Note that this will override the current theme and so may not display well
**`backgroundColor`** | `string` | _Optional_ | An optional background fill color for the that given item. Again, this will override the current theme and so might not display well against the background
**[⬆️ Back to Top](#configuring)**
#### `section.displayData` _(optional)_
### `section.displayData` _(optional)_
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
@ -93,21 +135,21 @@ All fields are optional, unless otherwise stated.
**`itemSize`** | `string` | _Optional_ | Specify the size for items within this group, either `small`, `medium` or `large`. Note that this will overide any settings specified through the UI
**`rows`** | `number` | _Optional_ | Height of the section, specified as the number of rows it should span vertically, e.g. `2`. Defaults to `1`. Max is `5`.
**`cols`** | `number` | _Optional_ | Width of the section, specified as the number of columns the section should span horizontally, e.g. `2`. Defaults to `1`. Max is `5`.
**`layout`** | `string` | _Optional_ | Specify which CSS layout will be used to responsivley place items. Can be either `auto` (which uses flex layout), or `grid`. If `grid` is selected, then `itemCountX` and `itemCountY` may also be set. Defaults to `auto`
**`itemCountX`** | `number` | _Optional_ | The number of items to display per row / horizontally. If not set, it will be calculated automatically based on available space. Can only be set if `layout` is set to `grid`. Must be a whole number between `1` and `12`
**`itemCountY`** | `number` | _Optional_ | The number of items to display per column / vertically. If not set, it will be calculated automatically based on available space. If `itemCountX` is set, then `itemCountY` can be calculated automatically. Can only be set if `layout` is set to `grid`. Must be a whole number between `1` and `12`
**`sectionLayout`** | `string` | _Optional_ | Specify which CSS layout will be used to responsivley place items. Can be either `auto` (which uses flex layout), or `grid`. If `grid` is selected, then `itemCountX` and `itemCountY` may also be set. Defaults to `auto`
**`itemCountX`** | `number` | _Optional_ | The number of items to display per row / horizontally. If not set, it will be calculated automatically based on available space. Can only be set if `sectionLayout` is set to `grid`. Must be a whole number between `1` and `12`
**`itemCountY`** | `number` | _Optional_ | The number of items to display per column / vertically. If not set, it will be calculated automatically based on available space. If `itemCountX` is set, then `itemCountY` can be calculated automatically. Can only be set if `sectionLayout` is set to `grid`. Must be a whole number between `1` and `12`
**[⬆️ Back to Top](#configuring)**
#### `section.icon` and `section.item.icon`
### `section.icon` and `section.item.icon`
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`icon`** | `string` | _Optional_ | The icon for a given item or section. Can be a font-awesome icon, favicon, remote URL or local URL. If set to `favicon`, the icon will be automatically fetched from the items website URL. To use font-awesome, specify the category, followed by the icon name, e.g. `fas fa-rocket`, `fab fa-monero` or `fal fa-duck` - note that to use pro icons, you mut specify `appConfig.fontAwesomeKey`. You can also use hosted any by specifying it's URL, e.g. `https://i.ibb.co/710B3Yc/space-invader-x256.png`. To use a local image, first store it in `./public/item-icons/` (or `-v /app/public/item-icons/` in Docker) , and reference it by name and extension - e.g. set `image.png` to use `./public/item-icon/image.png`, you can also use sub-folders if you have a lot of icons, to keep them organised.
**`icon`** | `string` | _Optional_ | The icon for a given item or section. Can be a font-awesome icon, favicon, remote URL or local URL. If set to `favicon`, the icon will be automatically fetched from the items website URL. To use font-awesome, specify the category, followed by the icon name, e.g. `fas fa-rocket`, `fab fa-monero` or `fal fa-duck` - note that to use pro icons, you mut specify `appConfig.fontAwesomeKey`. If set to `generative`, then a unique icon is generated from the apps URL or IP. You can also use hosted any by specifying it's URL, e.g. `https://i.ibb.co/710B3Yc/space-invader-x256.png`. To use a local image, first store it in `./public/item-icons/` (or `-v /app/public/item-icons/` in Docker) , and reference it by name and extension - e.g. set `image.png` to use `./public/item-icon/image.png`, you can also use sub-folders if you have a lot of icons, to keep them organised.
**[⬆️ Back to Top](#configuring)**
#### Example
### Example
```yaml
---
@ -140,5 +182,9 @@ sections: # An array of sections
For more example config files, see: [this gist](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10)
If you need any help, feel free to [Raise an Issue](https://github.com/Lissy93/dashy/issues/new?assignees=Lissy93&labels=%F0%9F%A4%B7%E2%80%8D%E2%99%82%EF%B8%8F+Question&template=question.md&title=%5BQUESTION%5D) or [Start a Discussion](https://github.com/Lissy93/dashy/discussions)
Happy Configuring 🤓🔧
**[⬆️ Back to Top](#configuring)**

View File

@ -83,6 +83,9 @@ on how to create a pull request..
8. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
with a clear title and description.
You can use emojis in your commit message, to indicate the category of the task.
For a reference of what each emoji means in the context of commits, see [gitmoji.dev](https://gitmoji.dev/).
#### Testing the Production App
For larger pull requests, please also check that it works as expected in a production environment.
@ -128,7 +131,8 @@ Click one of the links below, to open an issue:
### Contributors
![Auto-generated contributors](https://raw.githubusercontent.com/Lissy93/dashy/03fbaf35ff4653d16a622cfce00a1347c13d0192/docs/assets/CONTRIBUTORS.svg)
![Auto-generated contributors](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/CONTRIBUTORS.svg)
### Star-Gazers Over Time

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)
@ -9,6 +9,7 @@
- [Basic Commands](#basic-commands)
- [Healthchecks](#healthchecks)
- [Monitoring](#logs-and-performance)
- [Auto Starting](#auto-starting-at-system-boot)
- [Updating](#updating)
- [Updating Docker Container](#updating-docker-container)
- [Automating Docker Updates](#automatic-docker-updates)
@ -17,7 +18,7 @@
- [NGINX](#nginx)
- [Apache](#apache)
## Deployment
## Running the App
### Deploy with Docker
@ -49,6 +50,16 @@ You can also build and deploy the Docker container from source.
- Edit the `./public/conf.yml` file and take a look at the `docker-compose.yml`
- Start the container: `docker compose up`
### Other Container Engines
Docker isn't the only host application capable of running standard Linux containers - [Podman](http://podman.io) is another popular option. Unlike Docker, Podman does not rely on a daemon to be running on your host system. This means there is no single point of failure and it can also support rootless containers, which is perfect for Dashy as it does not require any sudo privileges. Podman was developed by RedHat, and it's source code is written in Go, and published on [GitHub](https://github.com/containers/podman).
Installation of the podman is really easy, as it's repository is available for most package managers (for example; Arch: `sudo pacman -S podman`, Debian/ Ubuntu: `sudo apt-get install podman`, Gentoo: `sudo emerge app-emulation/podman`, and MacOS: `brew install podman`). For more info, check out the [podman installation docs](https://podman.io/getting-started/installation). If you are using Windows, then take a look at Brent Baude's article on [Running Podman on WSL](https://www.redhat.com/sysadmin/podman-windows-wsl2). Since it's CLI is pretty much identical to that of Dockers, Podman's learning curve is very shallow.
To run Dashy with Podman, just replace `docker` with `podman` in the above instructions. E.g. `podman run -p 8080:80 lissy93/dashy`
It's worth noting that Podman isn't the only container running alternative, there's also [`rkt`](https://www.openshift.com/learn/topics/rkt), [`runc`](https://github.com/opencontainers/runc), [`containerd`](https://containerd.io/) and [`cri-o`](https://cri-o.io/). Dashy has not been tested with any of these engines, but it should work just fine.
### Deploy from Source
If you do not want to use Docker, you can run Dashy directly on your host system. For this, you will need both [git](https://git-scm.com/downloads) and the latest or LTS version of [Node.js](https://nodejs.org/) installed.
@ -63,7 +74,7 @@ If you do not want to use Docker, you can run Dashy directly on your host system
Dashy supports 1-Click deployments on several popular cloud platforms.
#### Netlify
#### Netlify <img src="https://i.ibb.co/ZxtzrP3/netlify.png" width="24"/>
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/lissy93/dashy)
[Netlify](https://www.netlify.com/) offers Git-based serverless cloud hosting for web applications. Their services are free to use for personal use, and they support deployment from both public and private repos, as well as direct file upload. The free plan also allows you to use your own custom domain or sub-domain, and is easy to setup.
@ -73,7 +84,7 @@ To deploy Dashy to Netlify, use the following link
https://app.netlify.com/start/deploy?repository=https://github.com/lissy93/dashy
```
#### Heroku
#### Heroku <img src="https://i.ibb.co/d2P1WZ7/heroku.png" width="24"/>
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/Lissy93/dashy)
[Heroku](https://www.heroku.com/) is a fully managed cloud platform as a service. You define app settings in a Procfile and app.json, which specifying how the app should be build and how the server should be started. Heroku is free to use for unlimited, non-commercial, single dyno apps, and supports custom domains. Heroku's single-dyno service is not as quite performant as some other providers, and the app will have a short wake-up time when not visited for a while
@ -83,7 +94,7 @@ To deploy Dashy to Heroku, use the following link
https://heroku.com/deploy?template=https://github.com/Lissy93/dashy
```
#### Cloudflare Workers
#### Cloudflare Workers <img src="https://i.ibb.co/CvpFM1S/cloudflare.png" width="24"/>
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/lissy93/dashy/tree/deploy_cloudflare)
[Cloudflare Workers](https://workers.cloudflare.com/) is a simple yet powerful service for running cloud functions and hosting web content. It requires a Cloudflare account, but is completely free for smaller projects, and very reasonably priced ($0.15/million requests per month) for large applications. You can use your own domain, and applications are protected with Cloudflare's state of the art DDoS protection. For more info, see the docs on [Worker Sites](https://developers.cloudflare.com/workers/platform/sites)
@ -93,7 +104,7 @@ To deploy Dashy to Cloudflare, use the following link
https://deploy.workers.cloudflare.com/?url=https://github.com/lissy93/dashy/tree/deploy_cloudflare
```
#### Deploy to Vercel
#### Vercel <img src="https://i.ibb.co/Ld2FZzb/vercel.png" width="24"/>
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/project?template=https://github.com/lissy93/dashy)
[Vercel](https://vercel.com/) is a performance-focused platform for hosting static frontend apps. It comes bundled with some useful tools for monitoring and anaylzing application performance and other metrics. Vercel is free for personal use, allows for custom domains and has very reasonable limits.
@ -103,7 +114,7 @@ To deploy Dashy to Vercel, use the following link
https://vercel.com/new/project?template=https://github.com/lissy93/dashy
```
#### Deploy to DigitalOcean
#### DigitalOcean <img src="https://i.ibb.co/V2MxtGC/digitalocean.png" width="24"/>
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/lissy93/dashy/tree/deploy_digital-ocean&refcode=3838338e7f79)
[DigitalOcan](https://www.digitalocean.com/) is a cloud service providing affordable developer-friendly virtual machines from $5/month. But they also have an app platform, where you can run web apps, static sites, APIs and background workers. CDN-backed static sites are free for personal use.
@ -112,7 +123,7 @@ https://vercel.com/new/project?template=https://github.com/lissy93/dashy
https://cloud.digitalocean.com/apps/new?repo=https://github.com/lissy93/dashy/tree/deploy_digital-ocean
```
#### Platform.sh
#### Platform.sh <img src="https://i.ibb.co/GdfvH3Z/platformsh.png" width="24"/>
[![Deploy to Platform.sh](https://platform.sh/images/deploy/deploy-button-lg-blue.svg)](https://console.platform.sh/projects/create-project/?template=https://github.com/lissy93/dashy&utm_campaign=deploy_on_platform?utm_medium=button&utm_source=affiliate_links&utm_content=https://github.com/lissy93/dashy)
[Platform.sh](https://platform.sh) is an end-to-end solution for developing and deploying applications. It is geared towards enterprise users with large teams, and focuses on allowing applications to scale up and down. Unlike the above providers, Platform.sh is not free, although you can deploy a test app to it without needing a payment method
@ -122,7 +133,17 @@ To deploy Dashy to Platform.sh, use the following link
https://console.platform.sh/projects/create-project/?template=https://github.com/lissy93/dashy
```
#### Deploy to Scalingo
#### Render <img src="https://i.ibb.co/xCHtzgh/render.png" width="24"/>
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/lissy93/dashy/tree/deploy_render)
[Render](https://render.com) is cloud provider that provides easy deployments for static sites, Docker apps, web services, databases and background workers. Render is great for developing applications, and very easy to use. Static sites are free, and services start at $7/month. Currently there are only 2 server locations - Oregon, USA and Frankfurt, Germany. For more info, see the [Render Docs](https://render.com/docs)
To deploy Dashy to Render, use the following link
```
https://render.com/deploy?repo=https://github.com/lissy93/dashy/tree/deploy_render
```
#### Scalingo <img src="https://i.ibb.co/Rvf5c4y/scalingo.png" width="24"/>
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/lissy93/dashy#master)
[Scalingo](https://scalingo.com/) is a scalable container-based cloud platform as a service. It's focus is on compliance and uptime, and is geared towards enterprise users. Scalingo is also not free, although they do have a 3-day free trial that does not require a payment method
@ -132,7 +153,7 @@ To deploy Dashy to Scalingo, use the following link
https://my.scalingo.com/deploy?source=https://github.com/lissy93/dashy#master
```
#### Play-with-Docker
#### Play-with-Docker <img src="https://i.ibb.co/HVWVYF7/docker.png" width="24"/>
[![Try in PWD](https://raw.githubusercontent.com/play-with-docker/stacks/cff22438/assets/images/button.png)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml)
[Play with Docker](https://labs.play-with-docker.com/) is a community project by Marcos Liljedhal and Jonathan Leibiusky and sponsored by Docker, intended to provide a hands-on learning environment. Their labs let you quickly spin up a Docker container or stack, and test out the image in a temporary, sandboxed environment. There's no need to sign up, and it's completely free.
@ -142,7 +163,7 @@ To run Dashy in PWD, use the following URL:
https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml
```
#### Surge.sh
#### Surge.sh <img src="https://i.ibb.co/WgVC4mB/surge.png" width="24"/>
[Surge.sh](http://surge.sh/) is quick and easy static web publishing platform for frontend-apps.
Surge supports [password-protected projects](https://surge.sh/help/adding-password-protection-to-a-project). You can also [add a custom domain](https://surge.sh/help/adding-a-custom-domain) and then [force HTTPS by default](https://surge.sh/help/using-https-by-default) and optionally [set a custom SSL certificate](https://surge.sh/help/securing-your-custom-domain-with-ssl)
@ -154,7 +175,7 @@ yarn build
surge ./dist
```
**[⬆️ Back to Top](#getting-started)**
**[⬆️ Back to Top](#deployment)**
---
@ -177,6 +198,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
@ -186,13 +208,46 @@ To restart unhealthy containers automatically, check out [Autoheal](https://hub.
### Logs and Performance
##### Container Logs
You can view logs for a given Docker container with `docker logs [container-id]`, add the `--follow` flag to stream the logs. For more info, see the [Logging Documentation](https://docs.docker.com/config/containers/logging/). There's also [Dozzle](https://dozzle.dev/), a useful tool, that provides a web interface where you can stream and query logs from all your running containers from a single web app.
##### Container Performance
You can check the resource usage for your running Docker containers with `docker stats` or `docker stats [container-id]`. For more info, see the [Stats Documentation](https://docs.docker.com/engine/reference/commandline/stats/). There's also [cAdvisor](https://github.com/google/cadvisor), a useful web app for viewing and analyzing resource usage and performance of all your running containers.
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.
##### Management Apps
You can also view logs, resource usage and other info as well as manage your entire Docker workflow in third-party Docker management apps. For example [Portainer](https://github.com/portainer/portainer) an all-in-one open source 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)**
##### Advanced Logging and Monitoring
Docker supports using [Prometheus](https://prometheus.io/) to collect logs, which can then be visualized using a platform like [Grafana](https://grafana.com/). For more info, see [this guide](https://docs.docker.com/config/daemon/prometheus/). If you need to route your logs to a remote syslog, then consider using [logspout](https://github.com/gliderlabs/logspout). For enterprise-grade instances, there are managed services, that make monitoring container logs and metrics very easy, such as [Sematext](https://sematext.com/blog/docker-container-monitoring-with-sematext/) with [Logagent](https://github.com/sematext/logagent-js).
### Auto-Starting at System Boot
You can use Docker's [restart policies](https://docs.docker.com/engine/reference/run/#restart-policies---restart) to instruct the container to start after a system reboot, or restart after a crash. Just add the `--restart=always` flag to your Docker compose script or Docker run command. For more information, see the docs on [Starting Containers Automatically](https://docs.docker.com/config/containers/start-containers-automatically/).
For Podman, you can use `systemd` to create a service that launches your container, [the docs](https://podman.io/blogs/2018/09/13/systemd.html) explains things further. A similar approach can be used with Docker, if you need to start containers after a reboot, but before any user interaction.
To restart the container after something within it has crashed, consider using [`docker-autoheal`](https://github.com/willfarrell/docker-autoheal) by @willfarrell, a service that monitors and restarts unhealthy containers. For more info, see the [Healthchecks](#healthchecks) section above.
### Securing
##### SSL
Enabling HTTPS with an SSL certificate is recommended if you hare hosting Dashy anywhere other than your home. This will ensure that all traffic is encrypted in transit.
[Let's Encrypt](https://letsencrypt.org/docs/) is a global Certificate Authority, providing free SSL/TLS Domain Validation certificates in order to enable secure HTTPS access to your website. They have good browser/ OS [compatibility](https://letsencrypt.org/docs/certificate-compatibility/) with their ISRG X1 and DST CA X3 root certificates, support [Wildcard issuance](https://community.letsencrypt.org/t/acme-v2-production-environment-wildcards/55578) done via ACMEv2 using the DNS-01 and have [Multi-Perspective Validation](https://letsencrypt.org/2020/02/19/multi-perspective-validation.html). Let's Encrypt provide [CertBot](https://certbot.eff.org/) an easy app for generating and setting up an SSL certificate
[ZeroSSL](https://zerossl.com/) is another popular certificate issuer, they are free for personal use, and also provide easy-to-use tools for getting things setup.
If you're hosting Dashy behind Cloudflare, then they offer [free and easy SSL](https://www.cloudflare.com/en-gb/learning/ssl/what-is-an-ssl-certificate/).
If you're not so comfortable on the command line, then you can use a tool like [SSL For Free](https://www.sslforfree.com/) to generate your Let's Encrypt or ZeroSSL certificate, and support shared hosting servers. They also provide step-by-step tutorials on setting up your certificate on most common platforms. If you are using shared hosting, you may find [this tutorial](https://www.sitepoint.com/a-guide-to-setting-up-lets-encrypt-ssl-on-shared-hosting/) helpful.
##### Authentication
Dashy has [basic authentication](/docs/authentication.md) built in, however at present this is handled on the front-end, and so where security is critical, it is recommended to use an alternative method. See [here](/docs/authentication.md#alternative-authentication-methods) for options regarding securing Dashy.
**[⬆️ Back to Top](#deployment)**
---
## Updating
@ -230,8 +285,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 +295,15 @@ _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.
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.
Note, that if you choose not to use `server.js` to serve up the app, you will loose access to the following features:
- Loading page, while the app is building
- Writing config file to disk from the UI
- Website status indicators, and ping checks
### NGINX
Create a new file in `/etc/nginx/sites-enabled/dashy`
@ -299,6 +360,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](#deployment)**
---
**[⬆️ Back to Top](#getting-started)**
## Authentication
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](#deployment)**

View File

@ -47,6 +47,26 @@ 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)
- `NODE_ENV` - Which environment to use, either `production`, `development` or `test`
- `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.
### Environment Modes
Both the Node app and Vue app supports several environments: `production`, `development` and `test`. You can set the environment using the `NODE_ENV` variable (either with your OS, in the Docker script or in an `.env` file - see [Environmental Variables](#environmental-variables) above).
The production environment will build the app in full, minifying and streamlining all assets. This means that building takes longer, but the app will then run faster. Whereas the dev environment creates a webpack configuration which enables HMR, doesn't hash assets or create vendor bundles in order to allow for fast re-builds when running a dev server. It supports sourcemaps and other debugging tools, re-compiles and reloads quickly but is not optimized, and so the app will not be as snappy as it could be. The test environment is intended for test running servers, it ignores assets that aren't needed for testing, and focuses on running all the E2E, regression and unit tests. For more information, see [Vue CLI Environment Modes](https://cli.vuejs.org/guide/mode-and-env.html#modes).
By default:
- `production` is used by `yarn build` (or `vue-cli-service build`) and `yarn build-and-start` and `yarn pm2-start`
- `development` is used by `yarn dev` (or `vue-cli-service serve`)
- `test` is used by `yarn test` (or `vue-cli-service test:unit`)
### 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 +99,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 +141,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: `./`

78
docs/icons.md Normal file
View File

@ -0,0 +1,78 @@
## Icons
Both sections and items can have an icon, which is specified using the `icon` attribute. Using icons improves the aesthetics of your UI and makes the app more intuitive to use. There are several options when it comes to setting icons, and this article outlines each of them
- [Font Awesome Icons](#font-awesome)
- [Auto-Fetched Favicons](#favicons)
- [Generative Icons](#generative-icons)
- [Emoji Icons](#emoji-icons)
- [Icons by URL](#icons-by-url)
- [Local Icons](#local-icons)
- [No Icon](#no-icon)
<p align="center">
<img width="500" src="https://i.ibb.co/GTVmZnc/dashy-example-icons.png" />
</p>
### Font Awesome
You can use any [Font Awesome Icon](https://fontawesome.com/icons) simply by specifying it's identifier. This is in the format of `[category] [name]` and can be found on the page for any given icon on the Font Awesome site. For example: `fas fa-rocket`, `fab fa-monero` or `fas fa-unicorn`.
Font-Awesome has a wide variety of free icons, but you can also use their pro icons if you have a membership. To do so, you need to specify your license key under: `appConfig.fontAwesomeKey`. This is usually a 10-digit string, for example `13014ae648`.
<p align="center">
<img width="580" src="https://i.ibb.co/pdrw8J4/fontawesome-icons2.png" />
</p>
### Favicons
Dashy can auto-fetch the favicon for a given service using it's URL. Just set `icon: favicon` to use this feature. If the services URL is a local IP, then Dashy will attempt to find the favicon from `http://[ip]/favicon.ico`. This has two issues, favicons are not always hosted at the same location for every service, and often the default favicon is a low resolution. Therefore to fix this, for remote services an API is used to return a high-quality icon for any online service.
<p align="center">
<img width="580" src="https://i.ibb.co/k6wyhnB/favicon-icons.png" />
</p>
The default favicon API is [Favicon Kit](https://faviconkit.com/), a free and reliable service for returning images from any given URL. However several other API's are supported. To change the API used, under `appConfig`, set `faviconApi` to one of the following values:
- `faviconkit` - [faviconkit.com](https://faviconkit.com/) (Recommend)
- `google` - Official Google favicon API service, good support for all sites, but poor quality
- `clearbit` - [Clearbit](https://clearbit.com/logo) returns high-quality logos from mainstream websites
- `webmasterapi` - [WebMasterAPI](https://www.webmasterapi.com/get-favicons)
- `allesedv` - [allesedv.com](https://favicon.allesedv.com/) is a highly efficient IPv6-enabled service
You can also force Dashy to always get favicons from the root of the domain, and not use an external service, by setting `appConfig.faviconApi` to `local`.
### Generative Icons
Uses a unique and programmatically generated icon for a given service. This is particularly useful when you have a lot of similar services with a different IP or port, and no specific icon. These icons are generated with [ipsicon.io](https://ipsicon.io/). To use this option, just set an item's to: `icon: generative`.
<p align="center">
<img width="400" src="https://i.ibb.co/qrNNNcm/generative-icons.png" />
</p>
### Emoji Icons
You can use almost any emoji as an icon for items or sections. You can specify the emoji either by pasting it directly, using it's unicode ( e.g. `'U+1F680'`) or shortcode (e.g. `':rocket:'`). You can find these codes for any emoji using [Emojipedia](https://emojipedia.org/) (near the bottom of emoji each page), or for a quick reference to emoji shortcodes, check out [emojis.ninja](https://emojis.ninja/) by @nomanoff.
<p align="center">
<img width="580" src="https://i.ibb.co/YLwgTf9/emoji-icons-1.png" />
</p>
The following example shows the unicode options available, all three will render the 🚀 emoji.
```yaml
items:
- title: Shortcode
icon: ':rocket:'
- title: Unicode
icon: 'U+1F680'
- title: Emoji
icon: 🚀
```
### Icons by URL
You can also set an icon by passing in a valid URL pointing to the icons location. For example `icon: https://i.ibb.co/710B3Yc/space-invader-x256.png`, this can be in .png, .jpg or .svg format, and hosted anywhere- so long as it's accessible from where you are hosting Dashy. The icon will be automatically scaled to fit, however loading in a lot of large icons may have a negative impact on performance, especially if you visit Dashy from new devices often.
### Local Icons
You may also want to store your icons locally, bundled within Dashy so that there is no reliance on outside services. This can be done by putting the icons within Dashy's `./public/item-icons/` directory. If you are using Docker, then the easiest option is to map a volume from your host system, for example: `-v /local/image/directory:/app/public/item-icons/`. To reference an icon stored locally, just specify it's name and extension. For example, if my icon was stored in `/app/public/item-icons/maltrail.png`, then I would just set `icon: maltrail.png`.
You can also use sub-folders within the `item-icons` directory to keep things organised. You would then specify an icon with it's folder name slash image name. For example: `networking/monit.png`
### No Icon
If you don't wish for a given item or section to have an icon, just leave out the `icon` attribute.

View File

@ -1,12 +1,13 @@
## Contents
- [Getting Started](/docs/getting-started.md)
- [Deployment](/docs/deployment.md)
- [Configuring](/docs/configuring.md)
- [Developing](/docs/developing.md)
- [Contributing](/docs/contributing.md)
- [User Guide](/docs/user-guide.md)
- [Troubleshooting](/docs/troubleshooting.md)
- [Backup & Restore](/docs/backup-restore.md)
- [Status Indicators](/docs/status-indicators.md)
- [Theming](/docs/theming.md)
- [Icons](/docs/icons.md)
- [Authentication](/docs/authentication.md)

92
docs/showcase.md Normal file
View File

@ -0,0 +1,92 @@
# *Dashy Showcase* 🌟
| 💗 Do you use Dashy? Got a sweet dashboard? Submit it to the showcase! 👉 [See How](#submitting-your-dashboard) |
|-|
### Home Lab 2.0
![screenshot-homelab](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/1-home-lab-material.png)
---
### Networking Services
> By [@Lissy93](https://github.com/lissy93)
![screenshot-networking-services](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/2-networking-services-minimal-dark.png)
---
### Homelab & VPS dashboard
> By [@shadowking001](https://github.com/shadowking001)
![screenshot-shadowking001-dashy](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/8-shadowking001s-dashy.png)
---
### NAS Home Dashboard
> By [@cerealconyogurt](https://github.com/cerealconyogurt)
![screenshot-networking-services](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/6-nas-home-dashboard.png)
---
### CFT Toolbox
![screenshot-cft-toolbox](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/3-cft-toolbox.png)
---
### Bookmarks
![screenshot-bookmarks](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/4-bookmarks-colourful.png)
---
### Project Management
![screenshot-project-managment](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/5-project-managment.png)
---
### Ground Control
> By [@dtctek](https://github.com/dtctek)
![screenshot-ground-control](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/7-ground-control-dtctek.png)
---
### Yet Another Homelab
![screenshot-yet-another-homelab](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/9-home-lab-oblivion.png)
---
## Submitting your Dashboard
#### How to Submit
- [Open an Issue](https://git.io/Jceik)
- [Open a PR](https://github.com/Lissy93/dashy/compare)
#### What to Include
Please include the following information:
- A single high-quality screenshot of your Dashboard
- A short title (it doesn't have to be particularly imaginative)
- An optional description, you could include details on anything interesting or unique about your dashboard, or say how you use it, and why it's awesome
- Optionally leave your name or username, with a link to your GitHub, Twitter or Website
#### Template
If you're submitting a pull request, please use a format similar to this:
```
### [Dashboard Name] (required)
> Submitted by [@username](https://github.com/user) (optional)
![dashboard-screenshot](/docs/showcase/screenshot-name.jpg) (required)
[An optional text description, or any interesting details] (optional)
---
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

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

@ -0,0 +1,76 @@
# 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
```
## Continuous Checking
By default, with status indicators enabled Dashy will check an applications status on page load, and will not keep indicators updated. This is usually desirable behavior. However, if you do want the status indicators to continue to poll your running services, this can be enabled by setting the `statusCheckInterval` attribute. Here you define an interval in seconds, and Dashy will poll your apps every x seconds. Note that if this number is very low (below 5 seconds), you may notice the app running slightly slower.
The following example, will instruct Dashy to continuously check the status of your services every 20 seconds
```
appConfig:
statusCheck: true
statusCheckInterval: 20
```
## Using a Different Endpoint
By default, the status checker will use the URL of each application being checked. In some situations, you may want to use a different endpoint for status checking. Similarly, some services provide a dedicated path for uptime monitoring.
You can set the `statusCheckUrl` property on any given item in order to do this. The status checker will then ping that endpoint, instead of the apps main `url` property.
## Setting Custom Headers
If your service is responding with an error, despite being up and running, it is most likely because custom headers for authentication, authorization or encoding are required. You can define these headers under the `statusCheckHeaders` property for any service. It should be defined as an object format, with the name of header as the key, and header content as the value.
For example, `statusCheckHeaders: { 'X-Custom-Header': 'foobar' }`
## Troubleshooting Failing Status Checks
If the status is always returning an error, despite the service being online, then it is most likely an issue with access control, and should be fixed with the correct headers. Hover over the failing status to see the error code and response, in order to know where to start with addressing it.
If your service requires requests to include any authorization in the headers, then use the `statusCheckHeaders` property, as described above.
If you are still having issues, it may be because your target application is blocking requests from Dashy's IP. This is a [CORS error](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), and can be fixed by setting the headers on your target app, to include:
```
Access-Control-Allow-Origin: https://location-of-dashy/
Vary: Origin
```
For further troubleshooting, use an application like [Postman](https://postman.com) to diagnose the issue.
## 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.
When the response completes, an indicator will display next to each item. The color denotes the status: 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. Requests are made asynchronously, so this won't have any impact on page load speeds. However recurring requests (using `statusCheckInterval`) may run more slowly if the interval between requests is very short.

View File

@ -4,7 +4,7 @@ By default Dashy comes with 20 built in themes, which can be applied from the dr
![Built-in Themes](https://i.ibb.co/GV3wRss/Dashy-Themes.png)
You can also add your own themes, apply custom CSS, and modify colors.
You can also add your own themes, apply custom styles, and modify colors.
You can customize Dashy by writing your own CSS, which can be loaded either as an external stylesheet, set directly through the UI, or specified in the config file. Most styling options can be set through CSS variables, which are outlined below.
@ -25,7 +25,7 @@ You can now create a block to target you're theme with `html[data-theme='my-them
```css
html[data-theme='tiger'] {
--primary: #f58233;
--item-group-background: #0b1021;
--background: #0b1021;
}
```
@ -33,17 +33,20 @@ Finally, from the UI use the theme dropdown menu to select your new theme, and y
You can also set `appConfig.theme` to pre-select a default theme, which will be applied immediately after deployment.
### Setting Custom CSS
### Adding your own Theme
User-defined styles and custom themes should be defined in `./src/styles/user-defined-themes.scss`. If you're using Docker, you can pass your own stylesheet in using the `--volume` flag. E.g. `v ./my-themes.scss:/app/src/styles/user-defined-themes.scss`. Don't forget to pass your theme name into `appConfig.cssThemes` so that it shows up on the theme-switcher dropdown.
### Setting Custom CSS in the UI
Custom CSS can be developed, tested and applied directly through the UI. Although you will need to make note of your changes to apply them across instances.
This can be done from the Config menu (spanner icon in the top-right), under the Custom Styles tab. This is then associated with `appConfig.customCss` in local storage. Any styles set this way can be synced across instances using the cloud backup and sync feature.
It's also possible to set CSS in the config file under `appConfig.customCss`. However this approach is not very neat, and if you do do it, first minify / compress your CSS and wrap in quotes, to ensure it does not cause any validation errors.
This can be done from the Config menu (spanner icon in the top-right), under the Custom Styles tab. This is then associated with `appConfig.customCss` in local storage. Styles can also be directly applied to this attribute in the config file, but this may get messy very quickly if you have a lot of CSS.
### Loading External Stylesheets
The URI of a stylesheet, either local or hosted on a remote CDN can be passed into the config file. The attribute `appConfig.externalStyleSheet` accepts either a string, or an array of strings. This is handled in [`ThemeHelper.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/ThemeHelper.js).
The URI of a stylesheet, either local or hosted on a remote CDN can be passed into the config file. The attribute `appConfig.externalStyleSheet` accepts either a string, or an array of strings. You can also pass custom font stylesheets here, they must be in a CSS format (for example, `https://fonts.googleapis.com/css2?family=Cutive+Mono`).
This is handled in [`ThemeHelper.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/ThemeHelper.js).
For example:
@ -63,6 +66,14 @@ Some UI components have a color option, that can be set in the config file, to f
- `item.color` - Font and icon color for a given item
- `item.backgroundColor` - Background color for a given icon
### Typography
Essential fonts bundled within the app are located within `./src/assets/fonts/`. All optional fonts that are used by themes are stored in `./public/fonts/`, if you want to add your own font, this is where you should put it. As with assets, if you're using Docker then using a volume to link a directory on your host system with this path within the container will make management much easier.
Fonts which are not being used by the current theme are **not** fetched on page load. They are instead only loaded into the application if and when they are required. So having multiple themes with various typefaces shouldn't have any negative impact on performance.
Full credit to the typographers behind each of the included fonts. Specifically: Matt McInerney, Christian Robertson, Haley Fiege, Peter Hull, Cyreal and the legendary Vernon Adams
### CSS Variables
All colors as well as other variable values (such as borders, border-radius, shadows) are specified as CSS variables. This makes theming the application easy, as you only need to change a given color or value in one place. You can find all variables in [`color-palette.scss`](https://github.com/Lissy93/dashy/blob/master/src/styles/color-palette.scss) and the themes which make use of these color variables are specified in [`color-themes.scss`](https://github.com/Lissy93/dashy/blob/master/src/styles/color-themes.scss)
@ -108,10 +119,21 @@ You can target specific elements on the UI with these variables. All are optiona
- `--config-settings-background` - The text color for text within the settings container. Defaults to `--background-darker`
- `--scroll-bar-color` - Color of the scroll bar thumb. Defaults to `--primary`
- `--scroll-bar-background` Color of the scroll bar blank space. Defaults to `--background-darker`
- `--highlight-background` Fill color for text highlighting. Defaults to `--primary`
- `--highlight-color` Text color for selected/ highlighted text. Defaults to `--background`
- `--toast-background` - Background color for the toast info popup. Defaults to `--primary`
- `--toast-color` - Text, icon and border color in the toast info popup. Defaults to `--background`
- `--welcome-popup-background` - Background for the info pop-up shown on first load. Defaults to `--background-darker`
- `--welcome-popup-text-color` - Text color for the welcome pop-up. Defaults to `--primary`
- `--side-bar-background` - Background color of the sidebar used in the workspace view. Defaults to `--background-darker`
- `--side-bar-color` - Color of icons and text within the sidebar. Defaults to `--primary`
- `--status-check-tooltip-background` - Background color for status check tooltips. Defaults to `--background-darker`
- `--status-check-tooltip-color` - Text color for the status check tooltips. Defaults to `--primary`
- `--code-editor-color` - Text color used within raw code editors. Defaults to `--black`
- `--code-editor-background` - Background color for raw code editors. Defaults to `--white`
- `--context-menu-color` - Text color for right-click context menu over items. Defaults to `--primary`
- `--context-menu-background` - Background color of right-click context menu. Defaults to `--background`
- `--context-menu-secondary-color` - Border and outline color for context menu. Defaults to `--background-darker`
#### Non-Color Variables
- `--outline-color` - Used to outline focused or selected elements

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

@ -1,21 +1,23 @@
{
"name": "Dashy",
"version": "0.1.0",
"version": "1.3.9",
"license": "MIT",
"main": "server",
"scripts": {
"start": "node server",
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint --fix",
"build-watch": "vue-cli-service build --watch",
"build-and-start": "npm-run-all --parallel build start",
"lint": "vue-cli-service lint",
"pm2-start": "npx pm2 start server.js",
"build-watch": "vue-cli-service build --watch --mode production",
"build-and-start": "npm-run-all --parallel build-watch start",
"validate-config": "node src/utils/ConfigValidator",
"health-check": "node bin/healthcheck"
"health-check": "node services/healthcheck"
},
"dependencies": {
"ajv": "^8.5.0",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"connect": "^3.7.0",
"crypto-js": "^4.0.0",
"highlight.js": "^11.0.0",
@ -31,7 +33,7 @@
"vue": "^2.6.10",
"vue-cli-plugin-yaml": "^1.0.2",
"vue-js-modal": "^2.0.0-rc.6",
"vue-material-tabs": "^0.0.7",
"vue-material-tabs": "^0.1.2",
"vue-prism-editor": "^1.2.2",
"vue-router": "^3.0.3",
"vue-select": "^3.11.2",
@ -47,10 +49,12 @@
"eslint": "^7.24.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-plugin-vue": "^7.9.0",
"progress-bar-webpack-plugin": "^2.1.0",
"sass": "^1.18.0",
"sass-loader": "^7.1.0",
"vue-svg-loader": "^0.16.0",
"vue-template-compiler": "^2.6.10"
"vue-template-compiler": "^2.6.10",
"webpack-build-notifier": "^2.3.0"
},
"gitHooks": {
"pre-commit": "yarn lint"
@ -83,4 +87,4 @@
"> 1%",
"last 2 versions"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7,7 +7,8 @@
<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">
<link rel="icon" type="image/png" sizes="64x64" href="/web-icons/favicon-64x64.png">
<link rel="icon" type="image/png" sizes="32x32" href="/web-icons/favicon-32x32.png">
<title>Dashy</title>
</head>

View File

@ -1,6 +1,14 @@
{
"name": "Dashy",
"name": "Dashy Web",
"short_name": "Dashy",
"description": "A Dashboard for your Homelab",
"scope": "/",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#0b1021",
"theme_color": "#4DBA87",
"lang": "en-GB",
"orientation": "portrait-primary",
"icons": [
{
"src": "./web-icons/windows10/SmallTile.scale-100.png",
@ -507,8 +515,42 @@
"sizes": "16x16"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#0b1021",
"theme_color": "#4DBA87"
"screenshots": [
{
"src": "./web-icons/screenshots/dashy-scrsht-1.png",
"sizes": "1523x1347",
"type": "image/png",
"label": "Dashy example homelab with Callisto theme"
},
{
"src": "./web-icons/screenshots/dashy-scrsht-2.png",
"sizes": "1264x861",
"type": "image/png",
"label": "Example, Networking services with Minimal Dark theme and a Horizontal layout"
},
{
"src": "./web-icons/screenshots/dashy-scrsht-3.png",
"sizes": "1303x864",
"type": "image/png",
"label": "Dashy example homelab with Material theme and auto-fetched favicons"
},
{
"src": "./web-icons/screenshots/dashy-scrsht-4.png",
"sizes": "1273x865",
"type": "image/png",
"label": "Dashy CFT Toolbox using Matrix theme"
},
{
"src": "./web-icons/screenshots/dashy-scrsht-5.png",
"sizes": "1146x851",
"type": "image/png",
"label": "Dashy as a Bookmark Manager, with Dracula theme and Font-Awesome icons"
},
{
"src": "./web-icons/screenshots/dashy-scrsht-6.png",
"sizes": "1147x872",
"type": "image/png",
"label": "Dashy example homelab with Nord theme"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

106
server.js
View File

@ -1,74 +1,92 @@
/* eslint-disable no-console */
/* This is a simple Node.js http server, that is used to serve up the contents of ./dist */
const connect = require('connect');
const serveStatic = require('serve-static');
/**
* Note: The app must first be built (yarn build) before this script is run
* This is the main entry point for the application, a simple server that
* runs some checks, and then serves up the app from the ./dist directory
* Also includes some routes for status checks/ ping and config saving
* */
/* Include required node dependencies */
const serveStatic = require('serve-static');
const connect = require('connect');
const util = require('util');
const dns = require('dns');
const os = require('os');
const bodyParser = require('body-parser');
require('./src/utils/ConfigValidator');
/* Include helper functions and route handlers */
const pingUrl = require('./services/ping'); // Used by the status check feature, to ping services
const saveConfig = require('./services/save-config'); // Saves users new conf.yml to file-system
const printMessage = require('./services/print-message'); // Function to print welcome msg on start
const rebuild = require('./services/rebuild-app'); // A script to programmatically trigger a build
require('./src/utils/ConfigValidator'); // Include and kicks off the config file validation script
/* Checks if app is running within a container, from env var */
const isDocker = !!process.env.IS_DOCKER;
/* Checks env var for port. If undefined, will use Port 80 for Docker, or 4000 for metal */
const port = process.env.PORT || (isDocker ? 80 : 4000);
/* Attempts to get the users local IP, used as part of welcome message */
const getLocalIp = () => {
const dnsLookup = util.promisify(dns.lookup);
return dnsLookup(os.hostname());
};
const overComplicatedMessage = (ip) => {
let msg = '';
const chars = {
RESET: '\x1b[0m',
CYAN: '\x1b[36m',
GREEN: '\x1b[32m',
BLUE: '\x1b[34m',
BRIGHT: '\x1b[1m',
BR: '\n',
};
const stars = (count) => new Array(count).fill('*').join('');
const line = (count) => new Array(count).fill('━').join('');
const blanks = (count) => new Array(count).fill(' ').join('');
if (isDocker) {
const containerId = process.env.HOSTNAME || undefined;
msg = `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}`
+ `${chars.CYAN}Welcome to Dashy! 🚀${chars.RESET}${chars.BR}`
+ `${chars.GREEN}Your new dashboard is now up and running `
+ `${containerId ? `in container ID ${containerId}` : 'with Docker'}${chars.BR}`
+ `${chars.GREEN}After updating your config file, run `
+ `'${chars.BRIGHT}docker exec -it ${containerId || '[container-id]'} yarn build`
+ `${chars.RESET}${chars.GREEN}' to rebuild${chars.BR}`
+ `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}`;
} else {
msg = `${chars.GREEN}${line(75)}${chars.BR}`
+ `${chars.CYAN}Welcome to Dashy! 🚀${blanks(55)}${chars.GREEN}${chars.BR}`
+ `${chars.CYAN}Your new dashboard is now up and running at ${chars.BRIGHT}`
+ `http://${ip}:${port}${chars.RESET}${blanks(18 - ip.length)}${chars.GREEN}${chars.BR}`
+ `${chars.CYAN}After updating your config file, run '${chars.BRIGHT}yarn build`
+ `${chars.RESET}${chars.CYAN}' to rebuild the app${blanks(6)}${chars.GREEN}${chars.BR}`
+ `${line(75)}${chars.BR}${chars.BR}`;
}
return msg;
};
/* eslint no-console: 0 */
/* Gets the users local IP and port, then calls to print welcome message */
const printWelcomeMessage = () => {
getLocalIp().then(({ address }) => {
const ip = address || 'localhost';
console.log(overComplicatedMessage(ip));
console.log(printMessage(ip, port, isDocker)); // eslint-disable-line no-console
});
};
/* Just console.warns an error */
const printWarning = (msg, error) => {
console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console
};
/* A middleware function for Connect, that filters requests based on method type */
const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, next) : next());
try {
connect()
.use(bodyParser.json())
// Serves up the main built application to the root
.use(serveStatic(`${__dirname}/dist`))
// During build, a custom page will be served before the app is available
.use(serveStatic(`${__dirname}/public`, { index: 'default.html' }))
// This root returns the status of a given service - used for uptime monitoring
.use('/ping', (req, res) => {
try {
pingUrl(req.url, async (results) => {
await res.end(results);
});
} catch (e) {
printWarning(`Error running ping check for ${req.url}\n`, e);
}
})
// POST Endpoint used to save config, by writing conf.yml to disk
.use('/config-manager/save', method('POST', (req, res) => {
try {
saveConfig(req.body, (results) => {
res.end(results);
});
} catch (e) {
res.end(JSON.stringify({ success: false, message: e }));
}
}))
// GET endpoint to trigger a build, and respond with success status and output
.use('/config-manager/rebuild', (req, res) => {
rebuild().then((response) => {
res.end(JSON.stringify(response));
}).catch((response) => {
res.end(JSON.stringify(response));
});
})
// Finally, initialize the server then print welcome message
.listen(port, () => {
try { printWelcomeMessage(); } catch (e) { console.log('Dashy is Starting...'); }
try { printWelcomeMessage(); } catch (e) { printWarning('Dashy is Starting...'); }
});
} catch (error) {
console.log('Sorry, an error occurred ', error);
printWarning('Sorry, a critical error occurred ', error);
}

View File

@ -1,37 +1,36 @@
/**
* An endpoint for confirming that the application is up and running
* Used for better Docker healthcheck results
* Note that exiting with code 1 indicates failure, and 0 is success
*/
const http = require('http');
/* Location of the server to test */
const port = process.env.PORT || !!process.env.IS_DOCKER ? 80 : 4000;
const host = process.env.HOST || '0.0.0.0';
const timeout = 2000;
const requestOptions = { host, port, timeout };
const startTime = new Date();
console.log(`[${startTime}] Running health check...`);
/* Starts quick HTTP server, attempts to send GET to app, then exists with appropriate exit code */
const healthCheck = http.request(requestOptions, (response) => {
const totalTime = (new Date() - startTime) / 1000;
const status = response.statusCode;
const color = status === 200 ? '\x1b[32m' : '\x1b[31m';
const message = `${color}Status: ${status}\nRequest took ${totalTime} seconds\n\x1b[0m---`;
console.log(message);
if (status == 200) { process.exit(0); }
else { process.exit(1); }
});
/* If the server is not running, then print the error code, and exit with 1 */
healthCheck.on('error', (err) => {
console.error(`\x1b[31mHealthceck Failed, Error: ${'\033[4m'}${err.code}\x1b[0m`);
process.exit(1);
});
healthCheck.end();
/**
* An endpoint for confirming that the application is up and running
* Used for better Docker healthcheck results
* Note that exiting with code 1 indicates failure, and 0 is success
*/
const http = require('http');
/* Location of the server to test */
const port = process.env.PORT || !!process.env.IS_DOCKER ? 80 : 4000;
const host = process.env.HOST || '0.0.0.0';
const timeout = 2000;
const requestOptions = { host, port, timeout };
const startTime = new Date(); // Initialize timestamp to calculate time taken
console.log(`[${startTime}] Running health check...`);
/* Starts quick HTTP server, attempts to send GET to app, then exists with appropriate exit code */
const healthCheck = http.request(requestOptions, (response) => {
const totalTime = (new Date() - startTime) / 1000;
const status = response.statusCode;
const color = status === 200 ? '\x1b[32m' : '\x1b[31m';
const message = `${color}Status: ${status}\nRequest took ${totalTime} seconds\n\x1b[0m---`;
console.log(message); // Print out healthcheck response
process.exit(status === 200 ? 0 : 1); // Exit with 0 (success), if response is 200 okay
});
/* If the server is not running, then print the error code, and exit with 1 */
healthCheck.on('error', (err) => {
console.error(`\x1b[31mHealthceck Failed, Error: ${'\x1b[33m'}${err.code}\x1b[0m`);
process.exit(1);
});
healthCheck.end();

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

55
services/print-message.js Normal file
View File

@ -0,0 +1,55 @@
/**
* Returns a welcome message, to be printed to the user when they start the app
* Contains essential info about restarting and managing the container or service
* @param String ip: The users local IP address or hostname
* @param Integer port: the port number that the app is running at
* @param Boolean isDocker: whether or not the app is being run within a container
* @returns A string formatted for the terminal
*/
module.exports = (ip, port, isDocker) => {
let msg = ''; // To return
const chars = { // Color codes used in the message
RESET: '\x1b[0m',
CYAN: '\x1b[36m',
GREEN: '\x1b[32m',
BLUE: '\x1b[34m',
BRIGHT: '\x1b[1m',
BR: '\n',
};
// Functions to insert string of set length of characters
const printChars = (count, char) => new Array(count).fill(char).join('');
const stars = (count) => printChars(count, '*');
const line = (count) => printChars(count, '━');
const blanks = (count) => printChars(count, ' ');
if (isDocker) {
// Prepare message for Docker users
const containerId = process.env.HOSTNAME || undefined;
msg = `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}`
+ `${chars.CYAN}Welcome to Dashy! 🚀${chars.RESET}${chars.BR}`
+ `${chars.GREEN}Your new dashboard is now up and running `
+ `${containerId ? `in container ID ${containerId}` : 'with Docker'}${chars.BR}`
+ `${chars.GREEN}After updating your config file, run `
+ `'${chars.BRIGHT}docker exec -it ${containerId || '[container-id]'} yarn build`
+ `${chars.RESET}${chars.GREEN}' to rebuild${chars.BR}`
+ `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}`;
} else {
// Prepare message for users running app on bare metal
msg = `${chars.GREEN}${line(75)}${chars.BR}`
+ `${chars.CYAN}Welcome to Dashy! 🚀${blanks(55)}${chars.GREEN}${chars.BR}`
+ `${chars.CYAN}Your new dashboard is now up and running at ${chars.BRIGHT}`
+ `http://${ip}:${port}${chars.RESET}${blanks(18 - ip.length)}${chars.GREEN}${chars.BR}`
+ `${chars.CYAN}After updating your config file, run '${chars.BRIGHT}yarn build`
+ `${chars.RESET}${chars.CYAN}' to rebuild the app${blanks(6)}${chars.GREEN}${chars.BR}`
+ `${line(75)}${chars.BR}${chars.BR}${chars.RESET}`;
}
// Make some sexy ascii art ;)
const ascii = `\x1b[40m${chars.CYAN}\n\n`
+ ' ██████╗ █████╗ ███████╗██╗ ██╗██╗ ██╗\n'
+ ' ██╔══██╗██╔══██╗██╔════╝██║ ██║╚██╗ ██╔╝\n'
+ ' ██║ ██║███████║███████╗███████║ ╚████╔╝\n'
+ ' ██║ ██║██╔══██║╚════██║██╔══██║ ╚██╔╝\n'
+ ' ██████╔╝██║ ██║███████║██║ ██║ ██║\n'
+ ` ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝\n${chars.RESET}\n`;
return ascii + msg;
};

34
services/rebuild-app.js Normal file
View File

@ -0,0 +1,34 @@
/**
* This script programmatically triggers a production build
* and responds with the status, message and full output
*/
const { exec } = require('child_process');
module.exports = () => new Promise((resolve, reject) => {
const buildProcess = exec('npm run build'); // Trigger the build command
let output = ''; // Will store console output
// Write output to console, and append to var for returning
buildProcess.stdout.on('data', (data) => {
process.stdout.write(data);
output += data;
});
// Handle errors, by sending the reject
buildProcess.on('error', (error) => {
reject(Error({
success: false,
error,
output,
}));
});
// When finished, check success, make message and resolve response
buildProcess.on('exit', (response) => {
const success = response === 0;
const message = `Build process exited with ${response}: `
+ `${success ? 'Success' : 'Possible Error'}`;
resolve({ success, message, output });
});
});

52
services/save-config.js Normal file
View File

@ -0,0 +1,52 @@
/**
* This file exports a function, used by the write config endpoint.
* It will make a backup of the users conf.yml file
* and then write their new config into the main conf.yml file.
* Finally, it will call a function with the status message
*/
const fsPromises = require('fs').promises;
module.exports = async (newConfig, render) => {
// Define constants for the config file
const settings = {
defaultLocation: './public/',
defaultFile: 'conf.yml',
filename: 'conf',
backupDenominator: '.backup.yml',
};
// Make the full file name and path to save the backup config file
const backupFilePath = `${settings.defaultLocation}${settings.filename}-`
+ `${Math.round(new Date() / 1000)}${settings.backupDenominator}`;
// The path where the main conf.yml should be read and saved to
const defaultFilePath = settings.defaultLocation + settings.defaultFile;
// Returns a string confirming successful job
const getSuccessMessage = () => `Successfully backed up ${settings.defaultFile} to`
+ ` ${backupFilePath}, and updated the contents of ${defaultFilePath}`;
// Encoding options for writing to conf file
const writeFileOptions = { encoding: 'utf8' };
// Prepare the response returned by the API
const getRenderMessage = (success, errorMsg) => JSON.stringify({
success,
message: !success ? errorMsg : getSuccessMessage(),
});
// Makes a backup of the existing config file
await fsPromises.copyFile(defaultFilePath, backupFilePath)
.catch((error) => {
render(getRenderMessage(false, `Unable to backup conf.yml: ${error}`));
});
// Writes the new content to the conf.yml file
await fsPromises.writeFile(defaultFilePath, newConfig.config.toString(), writeFileOptions)
.catch((error) => {
render(getRenderMessage(false, `Unable to write changes to conf.yml: ${error}`));
});
// If successful, then render hasn't yet been called- call it
await render(getRenderMessage(true));
};

View File

@ -3,7 +3,7 @@
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash()" />
<Header :pageInfo="pageInfo" />
<router-view />
<Footer v-if="showFooter" :text="getFooterText()" />
<Footer :text="getFooterText()" v-if="visibleComponents.footer" />
</div>
</template>
<script>
@ -11,8 +11,17 @@
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 { componentVisibility } from '@/utils/ConfigHelpers';
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import {
localStorageKeys,
splashScreenTime,
visibleComponents as defaultVisibleComponents,
} from '@/utils/defaults';
const Accumulator = new ConfigAccumulator();
const config = Accumulator.config();
const visibleComponents = componentVisibility(config.appConfig) || defaultVisibleComponents;
export default {
name: 'app',
@ -21,70 +30,48 @@ export default {
Footer,
LoadingScreen,
},
provide: {
config,
visibleComponents,
},
data() {
return {
// pageInfo: this.getPageInfo(conf.pageInfo),
showFooter: Defaults.visibleComponents.footer,
isLoading: true,
isLoading: true, // Set to false after mount complete
showFooter: visibleComponents.footer,
appConfig: Accumulator.appConfig(),
pageInfo: Accumulator.pageInfo(),
visibleComponents,
};
},
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;
},
/* If the user has specified custom text for footer - get it */
getFooterText() {
if (this.pageInfo && this.pageInfo.footerText) {
return this.pageInfo.footerText;
}
return '';
},
/* Injects the users custom CSS as a style tag */
injectCustomStyles(usersCss) {
const style = document.createElement('style');
style.textContent = usersCss;
document.head.append(style);
},
/* Determine if splash screen should be shown */
shouldShowSplash() {
return this.appConfig.showSplashScreen || !localStorage[localStorageKeys.HIDE_WELCOME_BANNER];
return (this.visibleComponents || defaultVisibleComponents).splashScreen
|| !localStorage[localStorageKeys.HIDE_WELCOME_BANNER];
},
/* Hide splash screen, either after 2 seconds, or immediately based on user preference */
hideSplash() {
if (this.shouldShowSplash()) {
setTimeout(() => { this.isLoading = false; }, splashScreenTime || 2000);
setTimeout(() => { this.isLoading = false; }, splashScreenTime || 1500);
} else {
this.isLoading = false;
}
},
},
/* When component mounted, hide splash and initiate the injection of custom styles */
mounted() {
this.hideSplash();
if (this.appConfig.customCss) {
@ -96,21 +83,12 @@ export default {
</script>
<style lang="scss">
/* Import styles used globally throughout the app */
@import '@/styles/global-styles.scss';
@import '@/styles/color-palette.scss';
@import '@/styles/dimensions.scss';
@import '@/styles/color-themes.scss';
@import '@/styles/typography.scss';
body {
background: var(--background);
margin: 0;
padding: 0;
}
#app {
.footer {
text-align: center;
}
}
@import '@/styles/user-defined-themes.scss';
</style>

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="info" class="svg-inline--fa fa-info fa-w-8" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path fill="currentColor" d="M224 352.589V224c0-16.475-6.258-31.517-16.521-42.872C225.905 161.14 236 135.346 236 108 236 48.313 187.697 0 128 0 68.313 0 20 48.303 20 108c0 20.882 5.886 40.859 16.874 58.037C15.107 176.264 0 198.401 0 224v39.314c0 23.641 12.884 44.329 32 55.411v33.864C12.884 363.671 0 384.359 0 408v40c0 35.29 28.71 64 64 64h128c35.29 0 64-28.71 64-64v-40c0-23.641-12.884-44.329-32-55.411zM128 48c33.137 0 60 26.863 60 60s-26.863 60-60 60-60-26.863-60-60 26.863-60 60-60zm80 400c0 8.836-7.164 16-16 16H64c-8.836 0-16-7.164-16-16v-40c0-8.836 7.164-16 16-16h16V279.314H64c-8.836 0-16-7.164-16-16V224c0-8.836 7.164-16 16-16h96c8.836 0 16 7.164 16 16v168h16c8.836 0 16 7.164 16 16v40z"></path></svg>

After

Width:  |  Height:  |  Size: 894 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="hammer" class="svg-inline--fa fa-hammer fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M571.31 193.94l-22.63-22.63c-6.25-6.25-16.38-6.25-22.63 0l-11.31 11.31-28.9-28.9c5.63-21.31.36-44.9-16.35-61.61l-45.25-45.25c-62.48-62.48-163.79-62.48-226.28 0l90.51 45.25v18.75c0 16.97 6.74 33.25 18.75 45.25l49.14 49.14c16.71 16.71 40.3 21.98 61.61 16.35l28.9 28.9-11.31 11.31c-6.25 6.25-6.25 16.38 0 22.63l22.63 22.63c6.25 6.25 16.38 6.25 22.63 0l90.51-90.51c6.23-6.24 6.23-16.37-.02-22.62zm-286.72-15.2c-3.7-3.7-6.84-7.79-9.85-11.95L19.64 404.96c-25.57 23.88-26.26 64.19-1.53 88.93s65.05 24.05 88.93-1.53l238.13-255.07c-3.96-2.91-7.9-5.87-11.44-9.41l-49.14-49.14z"></path></svg>

After

Width:  |  Height:  |  Size: 798 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sync" class="svg-inline--fa fa-sync fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M440.65 12.57l4 82.77A247.16 247.16 0 0 0 255.83 8C134.73 8 33.91 94.92 12.29 209.82A12 12 0 0 0 24.09 224h49.05a12 12 0 0 0 11.67-9.26 175.91 175.91 0 0 1 317-56.94l-101.46-4.86a12 12 0 0 0-12.57 12v47.41a12 12 0 0 0 12 12H500a12 12 0 0 0 12-12V12a12 12 0 0 0-12-12h-47.37a12 12 0 0 0-11.98 12.57zM255.83 432a175.61 175.61 0 0 1-146-77.8l101.8 4.87a12 12 0 0 0 12.57-12v-47.4a12 12 0 0 0-12-12H12a12 12 0 0 0-12 12V500a12 12 0 0 0 12 12h47.35a12 12 0 0 0 12-12.6l-4.15-82.57A247.17 247.17 0 0 0 255.83 504c121.11 0 221.93-86.92 243.55-201.82a12 12 0 0 0-11.8-14.18h-49.05a12 12 0 0 0-11.67 9.26A175.86 175.86 0 0 1 255.83 432z"></path></svg>

After

Width:  |  Height:  |  Size: 855 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="ellipsis-v-alt" class="svg-inline--fa fa-ellipsis-v-alt fa-w-6" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 512"><path fill="currentColor" d="M96 152c39.8 0 72-32.2 72-72S135.8 8 96 8 24 40.2 24 80s32.2 72 72 72zm0-112c22.1 0 40 17.9 40 40s-17.9 40-40 40-40-17.9-40-40 17.9-40 40-40zm0 144c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm0 112c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zm0 64c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm0 112c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40z"></path></svg>

After

Width:  |  Height:  |  Size: 671 B

View File

@ -1 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="th" class="svg-inline--fa fa-th fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M149.333 56v80c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24V56c0-13.255 10.745-24 24-24h101.333c13.255 0 24 10.745 24 24zm181.334 240v-80c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24h101.333c13.256 0 24.001-10.745 24.001-24zm32-240v80c0 13.255 10.745 24 24 24H488c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24H386.667c-13.255 0-24 10.745-24 24zm-32 80V56c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24h101.333c13.256 0 24.001-10.745 24.001-24zm-205.334 56H24c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24zM0 376v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zm386.667-56H488c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H386.667c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24zm0 160H488c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H386.667c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24zM181.333 376v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24z"></path></svg>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="table" class="svg-inline--fa fa-table fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zM224 416H64v-96h160v96zm0-160H64v-96h160v96zm224 160H288v-96h160v96zm0-160H288v-96h160v96z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 433 B

View File

@ -1 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="grip-horizontal" class="svg-inline--fa fa-grip-horizontal fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M96 288H32c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zm160 0h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zm160 0h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zM96 96H32c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zm160 0h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zm160 0h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32z"></path></svg>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="th-list" class="svg-inline--fa fa-th-list fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M149.333 216v80c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24v-80c0-13.255 10.745-24 24-24h101.333c13.255 0 24 10.745 24 24zM0 376v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zM125.333 32H24C10.745 32 0 42.745 0 56v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24zm80 448H488c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24zm-24-424v80c0 13.255 10.745 24 24 24H488c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24zm24 264H488c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24z"></path></svg>

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 1004 B

View File

@ -1 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="grip-vertical" class="svg-inline--fa fa-grip-vertical fa-w-10" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="currentColor" d="M96 32H32C14.33 32 0 46.33 0 64v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32V64c0-17.67-14.33-32-32-32zm0 160H32c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zm0 160H32c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zM288 32h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32V64c0-17.67-14.33-32-32-32zm0 160h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zm0 160h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32z"></path></svg>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="columns" class="svg-inline--fa fa-columns fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zM224 416H64V160h160v256zm224 0H288V160h160v256z"></path></svg>

Before

Width:  |  Height:  |  Size: 924 B

After

Width:  |  Height:  |  Size: 394 B

View File

@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="100px" height="100px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<defs>
<clipPath id="ldio-owbkoh4un5-cp">
<rect x="20" y="0" width="60" height="100"></rect>
</clipPath>
</defs>
<path
fill="none"
stroke="var(--primary, #00af87)"
stroke-width="6"
stroke-linecap="round"
stroke-linejoin="round"
stroke-miterlimit="10"
clip-path="url(#ldio-owbkoh4un5-cp)"
d="M90,76.7V28.3c0-2.7-2.2-5-5-5h-3.4c-2.7,0-5,2.2-5,5v43.4c0,2.7-2.2,5-5,5h-3.4c-2.7,0-5-2.2-5-5V28.3c0-2.7-2.2-5-5-5H55 c-2.7,0-5,2.2-5,5v43.4c0,2.7-2.2,5-5,5h-3.4c-2.7,0-5-2.2-5-5V28.3c0-2.7-2.2-5-5-5h-3.4c-2.7,0-5,2.2-5,5v43.4c0,2.7-2.2,5-5,5H15 c-2.7,0-5-2.2-5-5V23.3"
>
<animateTransform
attributeName="transform"
type="translate"
repeatCount="indefinite"
dur="1.4925373134328357s"
values="-20 0;7 0"
keyTimes="0;1"
></animateTransform>
<animate
attributeName="stroke-dasharray"
repeatCount="indefinite"
dur="1.4925373134328357s"
values="0 72 125 232;0 197 125 233"
keyTimes="0;1"></animate>
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,20 @@
<svg
aria-hidden="true"
focusable="false"
data-prefix="far"
data-icon="browser"
class="svg-inline--fa fa-browser fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
transform = "rotate(-90 250 250)"
fill="currentColor"
d="M464 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5
48-48V80c0-26.5-21.5-48-48-48zM48 92c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12
12v24c0 6.6-5.4 12-12 12H60c-6.6 0-12-5.4-12-12V92zm416 334c0 3.3-2.7 6-6
6H54c-3.3 0-6-2.7-6-6V168h416v258zm0-310c0 6.6-5.4 12-12 12H172c-6.6
0-12-5.4-12-12V92c0-6.6 5.4-12 12-12h280c6.6 0 12 5.4 12 12v24z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 697 B

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sign-out-alt" class="svg-inline--fa fa-sign-out-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M497 273L329 441c-15 15-41 4.5-41-17v-96H152c-13.3 0-24-10.7-24-24v-96c0-13.3 10.7-24 24-24h136V88c0-21.4 25.9-32 41-17l168 168c9.3 9.4 9.3 24.6 0 34zM192 436v-40c0-6.6-5.4-12-12-12H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h84c6.6 0 12-5.4 12-12V76c0-6.6-5.4-12-12-12H96c-53 0-96 43-96 96v192c0 53 43 96 96 96h84c6.6 0 12-5.4 12-12z"></path></svg>

After

Width:  |  Height:  |  Size: 584 B

View File

@ -0,0 +1,191 @@
<template>
<modal :name="modalName" :resizable="true" width="40%" height="60%" classes="dashy-modal">
<div class="about-modal">
<router-link to="/about">
<h2>Dashy V{{ appVersion }}</h2>
</router-link>
<h3>Service Worker Status</h3>
<code v-html="serviceWorkerInfo">{{ serviceWorkerInfo }}</code>
<br>
<h3>Config Validation Status</h3>
<code>{{getIsConfigValidStatus()}}</code>
<br>
<h3>Help & Support</h3>
<ul>
<li><a href="https://git.io/JnqPR">Report a Bug</a></li>
<li><a href="https://git.io/JnDxL">Request a Feature</a></li>
<li><a href="https://git.io/JnDxs">Ask a Question</a></li>
<li><a href="https://git.io/JnDxn">Leave Feedback</a></li>
<li><a href="https://github.com/Lissy93/dashy/discussions">Join the Discussion</a></li>
</ul>
<p class="small-note">Please include the following info in your bug report:</p>
<a @click="showInfo = !showInfo">{{ showInfo ? 'Hide' : 'Show'}} system info</a>
<div class="system-info" v-if="showInfo">
<h4>System Info</h4>
<code><b>Dashy Version:</b> V {{appVersion}}</code><br>
<code><b>Browser:</b> {{systemInfo.browser}}</code><br>
<code><b>Is Mobile?</b> {{systemInfo.isMobile ? 'Yes' : 'No'}}</code><br>
<code><b>OS:</b> {{systemInfo.os}}</code><br>
</div>
<h3>About</h3>
<p class="about-text">
Documentation and Source Code available on
<a href="https://github.com/lissy93/dashy">GitHub</a>
</p>
<h3>License</h3>
<code>Licensed under MIT X11. Copyright © 2021</code>
</div>
</modal>
</template>
<script>
import { modalNames, sessionStorageKeys } from '@/utils/defaults';
export default {
name: 'AppInfoModal',
data() {
return {
modalName: modalNames.ABOUT_APP,
appVersion: process.env.VUE_APP_VERSION,
systemInfo: this.getSystemInfo(),
serviceWorkerInfo: 'Checking...',
showInfo: false,
};
},
mounted() {
setTimeout(() => {
this.serviceWorkerInfo = this.getSwStatus();
}, 100);
},
methods: {
getIsConfigValidStatus() {
const isValidVar = process.env.VUE_APP_CONFIG_VALID;
if (isValidVar === undefined) return 'Config validation status is missing';
return `Config is ${isValidVar ? 'Valid' : 'Invalid'}`;
},
getSwStatus() {
const sessionData = sessionStorage[sessionStorageKeys.SW_STATUS];
const swInfo = sessionData ? JSON.parse(sessionData) : {};
let swStatus = '';
if (swInfo.registered) swStatus += 'Service worker registered<br>';
if (swInfo.ready) swStatus += 'Dashy is being served from service worker<br>';
if (swInfo.cached) swStatus += 'Content has been cached for offline use<br>';
if (swInfo.updateFound) swStatus += 'New content is downloading<br>';
if (swInfo.updated) swStatus += 'New content is available; please refresh<br>';
if (swInfo.offline) swStatus += 'No internet connection found. App is running in offline mode<br>';
if (swInfo.error) swStatus += 'Error during service worker registration<br>';
if (swInfo.devMode) swStatus += 'App running in dev mode, no need for service worker<br>';
if (swStatus.length === 0) swStatus += 'No service worker info available';
return swStatus;
},
getSystemInfo() {
const { userAgent } = navigator;
// Find Operating System
let os = 'Unknown';
if (userAgent.indexOf('Win') !== -1) os = 'Windows';
else if (userAgent.indexOf('Mac') !== -1) os = 'MacOS';
else if (userAgent.indexOf('Android') !== -1) os = 'Android';
else if (userAgent.indexOf('iPhone') !== -1) os = 'iOS';
else if (userAgent.indexOf('Linux') !== -1) os = 'Linux';
else if (userAgent.indexOf('X11') !== -1) os = 'UNIX';
// Find Browser
let browser = 'Unknown';
if (userAgent.indexOf('Opera') !== -1) browser = 'Opera';
else if (userAgent.indexOf('Chrome') !== -1) browser = 'Chrome';
else if (userAgent.indexOf('Safari') !== -1) browser = 'Safari';
else if (userAgent.indexOf('Firefox') !== -1) browser = 'Firefox';
else if (userAgent.indexOf('MSIE') !== -1) browser = 'IE';
else browser = 'Unknown';
const isMobile = !!navigator.userAgent.match(/iphone|android|blackberry/ig) || false;
return {
os,
browser,
userAgent,
isMobile,
};
},
},
};
</script>
<style scoped lang="scss">
span.options-label {
color: var(--settings-text-color);
}
.display-options {
color: var(--settings-text-color);
svg {
path {
fill: var(--settings-text-color);
}
width: 1rem;
height: 1rem;
margin: 0.2rem;
padding: 0.2rem;
text-align: center;
background: var(--background);
border: 1px solid currentColor;
border-radius: var(--curve-factor);
cursor: pointer;
&:hover, &.selected {
background: var(--settings-text-color);
path { fill: var(--background); }
}
}
}
div.about-modal {
background: var(--about-page-background);
color: var(--about-page-color);
padding: 1rem;
height: 100%;
hr {
border-color: var(--about-page-accent);
}
h2 {
text-decoration: none;
font-size: 1.8rem;
text-align: center;
margin: 0.2rem;
}
h3 {
font-size: 1.3rem;
margin: 0.75rem 0 0.2rem 0;
color: var(--about-page-accent);
}
p.small-note {
font-size: 0.9rem;
margin: 0.2rem 0;
}
p.about-text {
margin: 0.2rem 0;
}
a {
color: var(--about-page-accent);
}
ul {
margin-top: 0.2rem;
}
.system-info {
font-size: 0.8rem;
background: var(--black);
color: var(--white);
border-radius: var(--curve-factor-small);
padding: 0.5rem;
border: 1px solid var(--white);
width: fit-content;
h4 {
font-size: 0.8rem;
margin: 0 0 0.2rem 0;
text-decoration: underline;
}
}
}
</style>

View File

@ -3,21 +3,17 @@
<TabItem name="Config" class="main-tab">
<div class="main-options-container">
<h2>Configuration Options</h2>
<a href="/conf.yml" download class="hyperlink-wrapper">
<a class="hyperlink-wrapper" @click="downloadConfigFile('conf.yml', yaml)">
<button class="config-button center">
<DownloadIcon class="button-icon"/>
Download Config
</button>
</a>
<button class="config-button center" @click="goToEdit()">
<button class="config-button center" @click="() => navigateToTab(2)">
<EditIcon class="button-icon"/>
Edit Sections
Edit Config
</button>
<button class="config-button center" @click="goToMetaEdit()">
<MetaDataIcon class="button-icon"/>
Edit Meta Data
</button>
<button class="config-button center" @click="goToCustomCss()">
<button class="config-button center" @click="() => navigateToTab(3)">
<CustomCssIcon class="button-icon"/>
Edit Custom CSS
</button>
@ -25,37 +21,45 @@
<CloudIcon class="button-icon"/>
{{backupId ? 'Edit Cloud Sync' : 'Enable Cloud Sync'}}
</button>
<button class="config-button center" @click="openRebuildAppModal()">
<RebuildIcon class="button-icon"/>
Rebuild Application
</button>
<button class="config-button center" @click="resetLocalSettings()">
<DeleteIcon class="button-icon"/>
Reset Local Settings
</button>
<button class="config-button center" @click="openAboutModal()">
<IconAbout class="button-icon" />
App Info
</button>
<p class="small-screen-note" style="display: none;">
You are using a very small screen, and some screens in this menu may not be optimal
</p>
<p class="app-version">Dashy version {{ appVersion }}</p>
<div class="config-note">
<p class="sub-title">Note:</p>
<span>
All changes made here are stored locally. To apply globally, either export your config
into your conf.yml file, or use the cloud backup/ restore feature.
It is recommend to make a backup of your conf.yml file before making changes.
</span>
</div>
</div>
<!-- Rebuild App Modal -->
<RebuildApp />
</TabItem>
<TabItem name="Backup Config" class="code-container">
<pre id="conf-yaml">{{this.jsonParser(this.config)}}</pre>
<TabItem name="View Config" class="code-container">
<pre id="conf-yaml">{{yaml}}</pre>
<div class="yaml-action-buttons">
<h2>Actions</h2>
<a class="yaml-button download" href="/conf.yml" download>Download Config</a>
<a class="yaml-button download" @click="downloadConfigFile('conf.yml', yaml)">
Download Config
</a>
<a class="yaml-button copy" @click="copyConfigToClipboard()">Copy Config</a>
<a class="yaml-button reset" @click="resetLocalSettings()">Reset Config</a>
</div>
</TabItem>
<TabItem name="Edit Sections">
<TabItem name="Edit Config">
<JsonEditor :config="config" />
</TabItem>
<TabItem name="Edit Site Meta">
<EditSiteMeta :config="config" />
</TabItem>
<TabItem name="Custom Styles">
<CustomCssEditor :config="config" initialCss="hello" />
</TabItem>
@ -70,15 +74,17 @@ import 'highlight.js/styles/mono-blue.css';
import JsonToYaml from '@/utils/JsonToYaml';
import { localStorageKeys, modalNames } from '@/utils/defaults';
import EditSiteMeta from '@/components/Configuration/EditSiteMeta';
import JsonEditor from '@/components/Configuration/JsonEditor';
import CustomCssEditor from '@/components/Configuration/CustomCss';
import RebuildApp from '@/components/Configuration/RebuildApp';
import DownloadIcon from '@/assets/interface-icons/config-download-file.svg';
import DeleteIcon from '@/assets/interface-icons/config-delete-local.svg';
import EditIcon from '@/assets/interface-icons/config-edit-json.svg';
import MetaDataIcon from '@/assets/interface-icons/config-meta-data.svg';
import CustomCssIcon from '@/assets/interface-icons/config-custom-css.svg';
import CloudIcon from '@/assets/interface-icons/cloud-backup-restore.svg';
import RebuildIcon from '@/assets/interface-icons/application-rebuild.svg';
import IconAbout from '@/assets/interface-icons/application-about.svg';
export default {
name: 'ConfigContainer',
@ -86,6 +92,7 @@ export default {
return {
jsonParser: JsonToYaml,
backupId: localStorage[localStorageKeys.BACKUP_ID] || '',
appVersion: process.env.VUE_APP_VERSION,
};
},
props: {
@ -95,31 +102,33 @@ export default {
sections: function getSections() {
return this.config.sections;
},
yaml() {
return this.jsonParser(this.config);
},
},
components: {
EditSiteMeta,
JsonEditor,
CustomCssEditor,
RebuildApp,
DownloadIcon,
DeleteIcon,
EditIcon,
CloudIcon,
MetaDataIcon,
CustomCssIcon,
RebuildIcon,
IconAbout,
},
methods: {
/* Seletcs the edit tab of the tab view */
goToEdit() {
const itemToSelect = this.$refs.tabView.navItems[2];
this.$refs.tabView.activeTabItem({ tabItem: itemToSelect, byUser: true });
/* Progamatically navigates to a given tab by index */
navigateToTab(tabInxex) {
const itemToSelect = this.$refs.tabView.navItems[tabInxex];
this.$refs.tabView.activeTabItem(itemToSelect);
},
goToMetaEdit() {
const itemToSelect = this.$refs.tabView.navItems[3];
this.$refs.tabView.activeTabItem({ tabItem: itemToSelect, byUser: true });
openRebuildAppModal() {
this.$modal.show(modalNames.REBUILD_APP);
},
goToCustomCss() {
const itemToSelect = this.$refs.tabView.navItems[4];
this.$refs.tabView.activeTabItem({ tabItem: itemToSelect, byUser: true });
openAboutModal() {
this.$modal.show(modalNames.ABOUT_APP);
},
openCloudSync() {
this.$modal.show(modalNames.CLOUD_BACKUP);
@ -139,10 +148,20 @@ export default {
localStorage.clear();
this.$toasted.show('Data cleared succesfully');
setTimeout(() => {
location.reload(); // eslint-disable-line no-restricted-globals
location.reload(true); // eslint-disable-line no-restricted-globals
}, 1900);
}
},
/* Generates a new file, with the YAML contents, and triggers a download */
downloadConfigFile(filename, filecontents) {
const element = document.createElement('a');
element.setAttribute('href', `data:text/plain;charset=utf-8, ${encodeURIComponent(filecontents)}`);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
},
},
mounted() {
hljs.registerLanguage('yaml', yaml);
@ -194,6 +213,12 @@ a.config-button, button.config-button {
}
}
p.app-version {
margin: 0.5rem auto;
font-size: 1rem;
color: var(--transparent-white-50);
}
div.code-container {
background: var(--config-code-background);
#conf-yaml span {
@ -204,7 +229,7 @@ div.code-container {
}
.yaml-action-buttons {
position: absolute;
top: 0.5rem;
top: 1.5rem;
right: 0.5rem;
display: flex;
flex-direction: column;
@ -259,7 +284,7 @@ a.hyperlink-wrapper {
background: var(--config-settings-background);
height: calc(100% - 2rem);
h2 {
margin: 1rem auto;
margin: 0 auto 1rem auto;
color: var(--config-settings-color);
}
}
@ -274,14 +299,16 @@ a.hyperlink-wrapper {
border: 1px dashed var(--config-settings-color);
border-radius: var(--curve-factor);
text-align: left;
opacity: 0.95;
opacity: var(--dimming-factor);
color: var(--config-settings-color);
background: var(--config-settings-background);
cursor: default;
p.sub-title {
font-weight: bold;
margin: 0;
display: inline;
}
&:hover { opacity: 1; }
display: none;
@include tablet-up { display: block; }
}
@ -299,35 +326,50 @@ p.small-screen-note {
</style>
<style lang="scss">
.tabs__content {
height: -webkit-fill-available;
height: -moz-available;
height: stretch;
}
.tab-item {
background: var(--config-settings-background) !important;
}
.tab__pagination {
background: var(--config-settings-background);
color: var(--config-settings-color);
background: var(--config-settings-background) !important;
color: var(--config-settings-color) !important;
.tab__nav__items .tab__nav__item {
span {
color: var(--config-settings-color);
color: var(--config-settings-color) !important;
}
&:hover {
background: var(--config-settings-color) !important;
span {
color: var(--config-settings-background);
color: var(--config-settings-background) !important;
}
}
&.active {
span {
font-weight: bold;
font-weight: bold !important;
color: var(--config-settings-color) !important;
}
}
}
.tab__nav__items .tab__nav__item.active {
border-bottom: 2px solid var(--config-settings-color);
border-bottom: 2px solid var(--config-settings-color) !important;
}
hr.tab__slider {
background: var(--config-settings-color);
background: var(--config-settings-color) !important;
}
}
#conf-yaml .hljs-attr {
color: #9c03f5;
#conf-yaml {
background: var(--white);
.hljs-attr {
color: #9c03f5;
}
}
</style>

View File

@ -1,11 +1,27 @@
<template>
<div class="json-editor-outer">
<!-- Main JSON editor -->
<v-jsoneditor
v-model="jsonData"
:options="options"
height="580px"
height="500px"
/>
<!-- Options raido, and save button -->
<div class="save-options">
<span class="save-option-title">Save Location:</span>
<div class="option">
<input type="radio" id="local" value="local"
v-model="saveMode" class="radio-option" :disabled="!allowWriteToDisk" />
<label for="local" class="save-option-label">Apply Locally</label>
</div>
<div class="option">
<input type="radio" id="file" value="file" v-model="saveMode" class="radio-option"
:disabled="!allowWriteToDisk" />
<label for="file" class="save-option-label">Write Changes to Config File</label>
</div>
</div>
<button :class="`save-button ${!isValid ? 'err' : ''}`" @click="save()">Save Changes</button>
<!-- List validation warnings -->
<p class="errors">
<ul>
<li v-for="(error, index) in errorMessages" :key="index" :class="`type-${error.type}`">
@ -16,11 +32,19 @@
</li>
</ul>
</p>
<!-- Information notes -->
<p v-if="saveSuccess !== undefined"
:class="`response-output status-${saveSuccess ? 'success' : 'fail'}`">
{{saveSuccess ? 'Task Complete' : 'Task Failed'}}
</p>
<p class="response-output">{{ responseText }}</p>
<p v-if="saveSuccess" class="response-output">
The app should rebuild automatically.
This may take up to a minute.
You will need to refresh the page for changes to take effect.
</p>
<p class="note">
It is recommend to backup your existing confiruration before making any changes.
<br>
Remember that these changes are only applied locally,
and will need to be exported to your conf.yml
</p>
</div>
</template>
@ -30,6 +54,9 @@
import VJsoneditor from 'v-jsoneditor';
import { localStorageKeys } from '@/utils/defaults';
import configSchema from '@/utils/ConfigSchema.json';
import JsonToYaml from '@/utils/JsonToYaml';
import { isUserAdmin } from '@/utils/Auth';
import axios from 'axios';
export default {
name: 'JsonEditor',
@ -43,6 +70,7 @@ export default {
return {
jsonData: this.config,
errorMessages: [],
saveMode: 'file',
options: {
schema: configSchema,
mode: 'tree',
@ -50,6 +78,10 @@ export default {
name: 'config',
onValidationError: this.validationErrors,
},
jsonParser: JsonToYaml,
responseText: '',
saveSuccess: undefined,
allowWriteToDisk: this.shouldAllowWriteToDisk(),
};
},
computed: {
@ -57,8 +89,50 @@ export default {
return this.errorMessages.length < 1;
},
},
mounted() {
if (!this.allowWriteToDisk) this.saveMode = 'local';
},
methods: {
shouldAllowWriteToDisk() {
const { appConfig } = this.config;
return appConfig.allowConfigEdit !== false && isUserAdmin(appConfig.auth);
},
save() {
if (this.saveMode === 'local' || !this.allowWriteToDisk) {
this.saveConfigLocally();
} else if (this.saveMode === 'file') {
this.writeConfigToDisk();
} else {
this.$toasted.show('Please select a Save Mode: Local or File');
}
},
writeConfigToDisk() {
// 1. Convert JSON into YAML
const yaml = this.jsonParser(this.jsonData);
// 2. Prepare the request
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
const endpoint = `${baseUrl}/config-manager/save`;
const headers = { 'Content-Type': 'text/plain' };
const body = { config: yaml, timestamp: new Date() };
const request = axios.post(endpoint, body, headers);
// 3. Make the request, and handle response
request.then((response) => {
this.saveSuccess = response.data.success || false;
this.responseText = response.data.message;
if (this.saveSuccess) {
this.carefullyClearLocalStorage();
this.showToast('Config file written to disk succesfully', true);
} else {
this.showToast('An error occurred saving config', false);
}
})
.catch((error) => {
this.saveSuccess = false;
this.responseText = error;
this.showToast(error, false);
});
},
saveConfigLocally() {
const data = this.jsonData;
if (data.sections) {
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(data.sections));
@ -72,7 +146,12 @@ export default {
if (data.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, data.appConfig.theme);
}
this.$toasted.show('Changes saved succesfully');
this.showToast('Changes saved succesfully', true);
},
carefullyClearLocalStorage() {
localStorage.removeItem(localStorageKeys.PAGE_INFO);
localStorage.removeItem(localStorageKeys.APP_CONFIG);
localStorage.removeItem(localStorageKeys.CONF_SECTIONS);
},
validationErrors(errors) {
const errorMessages = [];
@ -100,11 +179,15 @@ export default {
});
this.errorMessages = errorMessages;
},
showToast(message, success) {
this.$toasted.show(message, { className: `toast-${success ? 'success' : 'error'}` });
},
},
};
</script>
<style lang="scss">
@import '@/styles/media-queries.scss';
.json-editor-outer {
text-align: center;
@ -138,6 +221,22 @@ p.errors {
}
}
}
p.response-output {
font-size: 0.8rem;
text-align: left;
margin: 0.5rem auto;
width: 95%;
color: var(--config-settings-color);
&.status-success {
font-weight: bold;
color: var(--success);
}
&.status-fail {
font-weight: bold;
color: var(--danger);
}
}
button.save-button {
padding: 0.5rem 1rem;
margin: 0.25rem auto;
@ -163,6 +262,37 @@ button.save-button {
}
}
div.save-options {
display: flex;
align-items: flex-start;
justify-content: center;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: var(--code-editor-background);
color: var(--code-editor-color);
border-top: 2px solid var(--config-settings-background);
@include tablet-down { flex-direction: column; }
.option {
@include tablet-up { margin-left: 2rem; }
}
span.save-option-title {
cursor: default;
}
input.radio-option {
cursor: pointer;
}
label.save-option-label {
cursor: pointer;
}
}
.jsoneditor, .jsoneditor-menu {
border-color: var(--primary);
}
.jsoneditor {
border-bottom: none;
}
.jsoneditor-menu, .pico-modal-header {
background: var(--config-settings-background) !important;
color: var(--config-settings-color) !important;
@ -182,7 +312,7 @@ div.jsoneditor-search div.jsoneditor-frame {
display: none;
}
.jsoneditor-tree, pre.jsoneditor-preview {
background: #fff;
background: var(--code-editor-background);
text-align: left;
}

View File

@ -0,0 +1,190 @@
<template>
<modal :name="modalName" :resizable="true" width="50%" height="60%" classes="dashy-modal">
<div class="rebuild-app-container">
<!-- Title, intro and start button -->
<h3 class="rebuild-app-title">Rebuild Application</h3>
<p>
A rebuild is required for changes written to the conf.yml file to take effect.
This should happen automatically, but if it hasn't, you can manually trigger it here.<br>
This is not required for modifications stored locally.
</p>
<Button :click="startBuild" :disabled="loading || !allowRebuild" :disallow="!allowRebuild">
<template v-slot:text>{{ loading ? 'Building...' : 'Start Build' }}</template>
<template v-slot:icon><RebuildIcon /></template>
</Button>
<div v-if="!allowRebuild">
<p class="disallow-rebuild-msg">You do no have permission to trigger this action</p>
</div>
<!-- Loading animation and text (shown while build is happening) -->
<div v-if="loading" class="loader-info">
<LoadingAnimation class="loader" />
<p class="loading-message">This may take a few minutes...</p>
</div>
<!-- Build response, and next actions (shown after build is done) -->
<div class="rebuild-response" v-if="success !== undefined">
<p v-if="success" class="response-status success"> Build completed succesfully</p>
<p v-else class="response-status failure"> Build operation failed</p>
<pre class="output"><code>{{ output || error }}</code></pre>
<p class="rebuild-message">{{ message }}</p>
<p v-if="success" class="rebuild-message">
A page reload is now required for changes to take effect
</p>
<Button :click="refreshPage" v-if="success">
<template v-slot:text>Reload Page</template>
<template v-slot:icon><ReloadIcon /></template>
</Button>
</div>
</div>
</modal>
</template>
<script>
import axios from 'axios';
import Button from '@/components/FormElements/Button';
import { modalNames } from '@/utils/defaults';
import RebuildIcon from '@/assets/interface-icons/application-rebuild.svg';
import ReloadIcon from '@/assets/interface-icons/application-reload.svg';
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
export default {
name: 'RebuildApp',
inject: ['config'],
components: {
Button,
RebuildIcon,
ReloadIcon,
LoadingAnimation,
},
data: () => ({
modalName: modalNames.REBUILD_APP,
loading: false,
success: undefined,
error: '',
output: '',
message: '',
allowRebuild: true,
}),
methods: {
startBuild() {
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
const endpoint = `${baseUrl}/config-manager/rebuild`;
this.loading = true;
axios.get(endpoint)
.then((response) => {
this.finished(response.data || false);
})
.catch((error) => {
this.finished({ success: false, error });
});
},
finished(responseData) {
this.loading = false;
if (responseData) {
const {
success, output, error, message,
} = responseData;
this.success = success;
this.output = output;
this.message = message;
this.error = error;
}
this.$toasted.show(
(this.success ? '✅ Build Completed Succesfully' : '❌ Build Failed'),
{ className: `toast-${this.success ? 'success' : 'error'}` },
);
},
refreshPage() {
location.reload(); // eslint-disable-line no-restricted-globals
},
},
mounted() {
if (this.config) {
if (this.config.appConfig) {
if (this.config.appConfig.allowConfigEdit === false) {
this.allowRebuild = false;
}
}
}
},
};
</script>
<style scoped lang="scss">
.rebuild-app-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 1rem;
color: var(--config-settings-color);
background: var(--config-settings-background);
overflow: auto;
button {
background: var(--config-settings-background);
color: var(--config-settings-color);
}
p.disallow-rebuild-msg {
color: var(--danger);
font-size: 1.2rem;
margin: 0.2rem auto;
text-align: center;
}
h3.rebuild-app-title {
text-align: center;
font-size: 2rem;
margin: 1rem;
}
div.loader-info {
margin: 0.2rem auto;
text-align: center;
svg.loader {
width: 100px;
}
p.loading-message {
margin: 0;
font-size: 0.8rem;
opacity: var(--dimming-factor);
animation: 3s fadeIn;
animation-fill-mode: forwards;
opacity: 0;
@keyframes fadeIn {
90% { opacity: 0; }
95% { opacity: 0.8; }
100% { opacity: 1; }
}
}
}
div.rebuild-response {
width: 80%;
margin: 0 auto 4rem auto;
text-align: center;
p.response-status {
font-size: 1rem;
text-align: left;
&.success {
color: var(--success);
}
&.failure {
color: var(--danger);
}
}
pre.output {
padding: 1rem;
font-size: 0.75rem;
border-radius: var(--curve-factor-small);
text-align: left;
color: var(--white);
background: var(--black);
white-space: pre-wrap;
}
p.rebuild-message {
font-size: 1rem;
text-align: left;
margin: 0.8rem 0;
color: var(--config-settings-color);
}
}
}
</style>

View File

@ -1,5 +1,6 @@
<template>
<button @click="click()">
<button @click="click()" :disabled="disabled" :class="disallow ? 'disallowed': ''">
<slot></slot>
<slot name="text"></slot>
<slot name="icon"></slot>
</button>
@ -12,6 +13,8 @@ export default {
props: {
text: String,
click: Function,
disabled: Boolean,
disallow: Boolean,
},
};
</script>
@ -36,6 +39,9 @@ button {
fill: currentColor;
}
}
&.disallowed {
cursor: not-allowed !important;
}
}
/* Default visual settings, can be overridden when needed */
@ -44,10 +50,14 @@ button {
background: var(--background);
border: 1px solid var(--primary);
border-radius: var(--curve-factor);
&:hover {
&:hover:not(:disabled) {
color: var(--background);
background: var(--primary);
border-color: var(--background);
}
&:disabled {
cursor: progress;
opacity: var(--dimming-factor);
}
}
</style>

View File

@ -11,7 +11,7 @@
tabIndex="-1"
>
<label :for="`collapsible-${uniqueKey}`" class="lbl-toggle" tabindex="-1">
<Icon v-if="icon" :icon="icon" size="small" class="section-icon" />
<Icon v-if="icon" :icon="icon" size="small" :url="title" class="section-icon" />
<h3>{{ title }}</h3>
</label>
<div class="collapsible-content">
@ -42,11 +42,6 @@ export default {
components: {
Icon,
},
data() {
return {
isOpen: !this.collapsed,
};
},
methods: {
/* Check that row & column span is valid, and not over the max */
checkSpanNum(span, classPrefix) {

View File

@ -0,0 +1,116 @@
<template>
<transition name="slide">
<div class="context-menu" v-if="show && menuEnabled"
:style="posX && posY ? `top:${posY}px;left:${posX}px;` : ''">
<ul>
<li @click="launch('sametab')">
<SameTabOpenIcon />
<span>Open in Current Tab</span>
</li>
<li @click="launch('newtab')">
<NewTabOpenIcon />
<span>Open in New Tab</span>
</li>
<li @click="launch('modal')">
<IframeOpenIcon />
<span>Open in Pop-Up Modal</span>
</li>
<li @click="launch('workspace')">
<WorkspaceOpenIcon />
<span>Open in Workspace View</span>
</li>
</ul>
</div>
</transition>
</template>
<script>
// Import icons for each element
import SameTabOpenIcon from '@/assets/interface-icons/open-current-tab.svg';
import NewTabOpenIcon from '@/assets/interface-icons/open-new-tab.svg';
import IframeOpenIcon from '@/assets/interface-icons/open-iframe.svg';
import WorkspaceOpenIcon from '@/assets/interface-icons/open-workspace.svg';
export default {
name: 'ContextMenu',
inject: ['config'],
components: {
SameTabOpenIcon,
NewTabOpenIcon,
IframeOpenIcon,
WorkspaceOpenIcon,
},
props: {
posX: Number, // The X coordinate for positioning
posY: Number, // The Y coordinate for positioning
show: Boolean, // Should show or hide the menu
},
data() {
return {
menuEnabled: !this.isMenuDisabled(), // Specifies if the context menu should be used
};
},
methods: {
/* Called on item click, emits an event up to Item */
/* in order to launch the current app to a given target */
launch(target) {
this.$emit('contextItemClick', target);
},
/* Checks if the user as disabled context menu in config */
isMenuDisabled() {
if (this.config && this.config.appConfig) {
return !!this.config.appConfig.disableContextMenu;
}
return false;
},
},
};
</script>
<style lang="scss">
div.context-menu {
position: absolute;
margin: 0;
padding: 0;
z-index: 8;
background: var(--context-menu-background);
color: var(--context-menu-color);
border: 1px solid var(--context-menu-secondary-color);
border-radius: var(--curve-factor);
box-shadow: var(--context-menu-shadow);
opacity: 0.98;
ul {
list-style-type: none;
margin: 0;
padding: 0;
li {
cursor: pointer;
padding: 0.5rem 1rem;
display: flex;
flex-direction: row;
font-size: 1rem;
&:not(:last-child) {
border-bottom: 1px solid var(--context-menu-secondary-color);
}
&:hover {
background: var(--context-menu-secondary-color);
}
svg {
width: 1rem;
margin-right: 0.5rem;
path { fill: currentColor; }
}
}
}
}
// Define enter and leave transitions
.slide-enter-active { animation: slide-in .1s; }
.slide-leave-active { animation: slide-in .1s reverse; }
@keyframes slide-in {
0% { transform: scaleY(0.5) scaleX(0.8) translateY(-50px); }
100% { transform: scaleY(1) translateY(0) translateY(0); }
}
</style>

View File

@ -1,5 +1,6 @@
<template>
<modal :name="name" :resizable="true" width="80%" height="80%" @closed="modalClosed()">
<modal :name="name" :resizable="true" width="80%" height="80%" @closed="modalClosed()"
classes="dashy-modal">
<div slot="top-right" @click="hide()">Close</div>
<a @click="hide()" class="close-button" title="Close">x</a>
<iframe v-if="url" :src="url" @keydown.esc="close" class="frame"/>
@ -17,12 +18,12 @@ export default {
url: '#',
}),
methods: {
show: function show(url) {
show(url) {
this.url = url;
this.$modal.show(this.name);
this.$emit('modalChanged', true);
},
hide: function hide() {
hide() {
this.$modal.hide(this.name);
},
modalClosed() {

View File

@ -1,6 +1,9 @@
<template ref="container">
<div class="item-wrapper">
<a @click="itemOpened"
:href="target !== 'iframe' ? url : '#'"
@mouseup.right="openContextMenu"
@contextmenu.prevent
:href="target !== 'modal' ? url : '#'"
:target="target === 'newtab' ? '_blank' : ''"
:class="`item ${!icon? 'short': ''} size-${itemSize}`"
v-tooltip="getTooltipOptions()"
@ -11,7 +14,6 @@
<!-- Item Text -->
<div :class="`tile-title ${!icon? 'bounce': ''}`" :id="`tile-${id}`" >
<span class="text">{{ title }}</span>
<div class="overflow-dots">...</div>
<p class="description">{{ description }}</p>
</div>
<!-- Item Icon -->
@ -20,12 +22,32 @@
<!-- Small icon, showing opening method on hover -->
<ItemOpenMethodIcon class="opening-method-icon" :isSmall="!icon" :openingMethod="target"
:position="itemSize === 'medium'? 'bottom right' : 'top right'"/>
<!-- Status indicator dot (if enabled) showing weather srevice is availible -->
<StatusIndicator
class="status-indicator"
v-if="enableStatusCheck"
:statusSuccess="statusResponse ? statusResponse.successStatus : undefined"
:statusText="statusResponse ? statusResponse.message : undefined"
/>
</a>
<ContextMenu
:show="contextMenuOpen"
v-click-outside="closeContextMenu"
:posX="contextPos.posX"
:posY="contextPos.posY"
:id="`context-menu-${id}`"
@contextItemClick="contextItemClick"
/>
</div>
</template>
<script>
import axios from 'axios';
import router from '@/router';
import Icon from '@/components/LinkItems/ItemIcon.vue';
import ItemOpenMethodIcon from '@/components/LinkItems/ItemOpenMethodIcon';
import StatusIndicator from '@/components/LinkItems/StatusIndicator';
import ContextMenu from '@/components/LinkItems/ContextMenu';
export default {
name: 'Item',
@ -38,47 +60,62 @@ export default {
color: String, // Optional text and icon color, specified in hex code
backgroundColor: String, // Optional item background color
url: String, // URL to the resource, optional but recommended
target: { // Where resource will open, either 'newtab', 'sametab' or 'iframe'
target: { // Where resource will open, either 'newtab', 'sametab' or 'modal'
type: String,
default: 'newtab',
validator: (value) => ['newtab', 'sametab', 'iframe'].indexOf(value) !== -1,
validator: (value) => ['newtab', 'sametab', 'modal', 'workspace'].indexOf(value) !== -1,
},
itemSize: String,
enableStatusCheck: Boolean,
statusCheckHeaders: Object,
statusCheckUrl: String,
statusCheckInterval: Number,
},
data() {
return {
contextMenuOpen: false,
getId: this.id,
customStyles: {
color: this.color,
background: this.backgroundColor,
},
statusResponse: undefined,
contextPos: {
posX: undefined,
posY: undefined,
},
};
},
components: {
Icon,
ItemOpenMethodIcon,
StatusIndicator,
ContextMenu,
},
methods: {
/* Called when an item is clicked, manages the opening of iframe & resets the search field */
/* Called when an item is clicked, manages the opening of modal & resets the search field */
itemOpened(e) {
if (e.altKey || this.target === 'iframe') {
if (e.altKey || this.target === 'modal') {
e.preventDefault();
this.$emit('triggerModal', this.url);
} else {
this.$emit('itemClicked');
}
},
/**
* Detects overflowing text, shows ellipse, and allows is to marguee on hover
* The below code is horifically bad, it is embarassing that I wrote it...
*/
manageTitleEllipse() {
const tileElem = document.getElementById(`tile-${this.getId}`);
if (tileElem) {
const isOverflowing = (tileElem.scrollHeight > tileElem.clientHeight
|| tileElem.scrollWidth > tileElem.clientWidth) && this.title.length > 12;
if (isOverflowing) tileElem.className += ' is-overflowing';
} // Note from present me to past me: WTF?!
/* Open custom context menu, and set position */
openContextMenu(e) {
this.contextMenuOpen = !this.contextMenuOpen;
if (e && window) {
// Calculate placement based on cursor and scroll position
this.contextPos = {
posX: e.clientX + window.pageXOffset,
posY: e.clientY + window.pageYOffset,
};
}
},
/* Closes the context menu, called when user clicks literally anywhere */
closeContextMenu() {
this.contextMenuOpen = false;
},
/* Returns configuration object for the tooltip */
getTooltipOptions() {
@ -88,26 +125,76 @@ export default {
trigger: 'hover focus',
hideOnTargetClick: true,
html: false,
placement: this.statusResponse ? 'left' : 'auto',
delay: { show: 600, hide: 200 },
classes: 'item-description-tooltip',
};
},
/* Used by certain themes, which display an icon with animated CSS */
getUnicodeOpeningIcon() {
switch (this.target) {
case 'newtab': return '"\\f360"';
case 'sametab': return '"\\f24d"';
case 'iframe': return '"\\f2d0"';
case 'modal': return '"\\f2d0"';
default: return '"\\f054"';
}
},
/* Checks if a given service is currently online */
checkWebsiteStatus() {
this.statusResponse = undefined;
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
const urlToCheck = this.statusCheckUrl || this.url;
const headers = this.statusCheckHeaders || {};
const endpoint = `${baseUrl}/ping?url=${urlToCheck}`;
axios.get(endpoint, { headers })
.then((response) => {
if (response.data) this.statusResponse = response.data;
})
.catch(() => {
this.statusResponse = {
statusText: 'Failed to make request',
statusSuccess: false,
};
});
},
/* Handle navigation options from the context menu */
contextItemClick(method) {
const { url } = this;
this.contextMenuOpen = false;
switch (method) {
case 'newtab':
window.open(url, '_blank');
break;
case 'sametab':
window.open(url, '_self');
break;
case 'modal':
this.$emit('triggerModal', url);
break;
case 'workspace':
router.push({ name: 'workspace', query: { url } });
break;
default: window.open(url, '_blank');
}
},
},
mounted() {
this.manageTitleEllipse();
// If ststus checking is enabled, then check service status
if (this.enableStatusCheck) this.checkWebsiteStatus();
// If continious status checking is enabled, then start ever-lasting loop
if (this.statusCheckInterval > 0) {
setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
}
},
};
</script>
<style lang="scss">
.item-wrapper {
flex-grow: 1;
}
.item {
flex-grow: 1;
color: var(--item-text-color);
@ -122,10 +209,16 @@ export default {
box-shadow: var(--item-shadow);
cursor: pointer;
text-decoration: none;
position: relative;
transition: all 0.2s ease-in-out 0s;
&:hover {
box-shadow: var(--item-hover-shadow);
background: var(--item-background-hover);
color: var(--item-text-color-hover);
position: relative;
.tile-title span.text {
white-space: pre-wrap;
}
}
&:focus {
outline: 2px solid var(--primary);
@ -142,39 +235,21 @@ export default {
text-overflow: ellipsis;
min-width: 120px;
height: 30px;
overflow: hidden;
position: relative;
padding: 0;
z-index: 2;
span.text {
position: absolute;
white-space: nowrap;
transition: 1s;
float: left;
left: 0;
}
&:not(.is-overflowing) span.text{
width: 100%;
}
.overflow-dots {
opacity: 0;
}
&.is-overflowing {
span.text {
overflow: hidden;
}
.overflow-dots {
display: block;
opacity: 1;
background: var(--item-background);
position: absolute;
z-index: 5;
right: 0;
transition: opacity 0.1s ease-in;
}
}
}
/* Colored dot showing service status */
.status-indicator {
position: absolute;
top: 0;
right: 0;
}
.opening-method-icon {
display: none; // Hidden by default, visible on hover
}
@ -204,24 +279,29 @@ export default {
/* Specify layout for alternate sized icons */
.item {
/* Small Tile Specific Themes */
&.size-small {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
align-items: center;
height: 2rem;
padding-top: 4px;
div img, div svg.missing-image {
width: 2rem;
}
.tile-title {
height: fit-content;
min-height: 1.2rem;
text-align: left;
max-width:140px;
span.text {
text-align: left;
padding-left: 10%;
}
}
}
/* Medium Tile Specific Themes */
&.size-medium {
display: flex;
flex-direction: column;
@ -233,16 +313,45 @@ export default {
}
.tile-title {
min-width: 100px;
max-width: 160px;
}
}
/* Large Tile Specific Themes */
&.size-large {
height: 100px;
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
text-align: left;
overflow: hidden;
align-items: center;
max-height: 6rem;
margin: 0.2rem;
padding: 0.5rem;
img {
padding: 0.1rem 0.25rem;
}
.tile-title {
height: auto;
padding: 0.1rem 0.25rem;
span.text {
position: relative;
font-weight: bold;
font-size: 1.1rem;
width: 100%;
}
p.description {
display: block;
margin: 0;
white-space: pre-wrap;
font-size: .9em;
text-overflow: ellipsis;
}
}
}
p.description {
display: none;
display: none; // By default, we don't show the description
}
&:before {
&:before { // Certain themes (e.g. material) show css animated fas icon on hover
display: none;
font-family: FontAwesome;
content: var(--open-icon, "\f054") !important;

View File

@ -27,7 +27,11 @@
:target="item.target"
:color="item.color"
:backgroundColor="item.backgroundColor"
:statusCheckUrl="item.statusCheckUrl"
:statusCheckHeaders="item.statusCheckHeaders"
:itemSize="newItemSize"
:enableStatusCheck="shouldEnableStatusCheck(item.statusCheck)"
:statusCheckInterval="getStatusCheckInterval()"
@itemClicked="$emit('itemClicked')"
@triggerModal="triggerModal"
/>
@ -49,6 +53,7 @@ import IframeModal from '@/components/LinkItems/IframeModal.vue';
export default {
name: 'ItemGroup',
inject: ['config'],
props: {
groupId: String,
title: String,
@ -68,7 +73,7 @@ export default {
return this.displayData.itemSize || this.itemSize;
},
isGridLayout() {
return this.displayData.layout === 'grid'
return this.displayData.sectionLayout === 'grid'
|| !!(this.displayData.itemCountX || this.displayData.itemCountY);
},
gridStyle() {
@ -92,6 +97,17 @@ export default {
modalChanged(changedTo) {
this.$emit('change-modal-visibility', changedTo);
},
shouldEnableStatusCheck(itemPreference) {
const globalPreference = this.config.appConfig.statusCheck || false;
return itemPreference !== undefined ? itemPreference : globalPreference;
},
getStatusCheckInterval() {
let interval = this.config.appConfig.statusCheckInterval;
if (!interval) return 0;
if (interval > 60) interval = 60;
if (interval < 1) interval = 0;
return interval;
},
},
};
</script>

View File

@ -1,6 +1,7 @@
<template>
<div>
<div class="item-icon">
<i v-if="iconType === 'font-awesome'" :class="`${icon} ${size}`" ></i>
<i v-else-if="iconType === 'emoji'" :class="`emoji-icon ${size}`" >{{getEmoji(iconPath)}}</i>
<img v-else-if="icon" :src="iconPath" @error="imageNotFound"
:class="`tile-icon ${size} ${broken ? 'broken' : ''}`"
/>
@ -11,9 +12,13 @@
<script>
import BrokenImage from '@/assets/interface-icons/broken-icon.svg';
import ErrorHandler from '@/utils/ErrorHandler';
import { faviconApi as defaultFaviconApi, faviconApiEndpoints } from '@/utils/defaults';
import EmojiUnicodeRegex from '@/utils/EmojiUnicodeRegex';
import emojiLookup from '@/utils/emojis.json';
export default {
name: 'Icon',
inject: ['config'],
props: {
icon: String, // Path to icon asset
url: String, // Used for fetching the favicon
@ -33,6 +38,7 @@ export default {
data() {
return {
broken: false,
// faviconApi: this.config.appConfig.faviconApi || defaultFaviconApi,
};
},
methods: {
@ -49,29 +55,61 @@ export default {
if (splitPath.length >= 1) return validImgExtensions.includes(splitPath[1]);
return false;
},
/* Determins if a given string is an emoji, and if so what type it is */
isEmoji(img) {
if (EmojiUnicodeRegex.test(img) && img.match(/./gu).length) { // Is a unicode emoji
return { isEmoji: true, emojiType: 'glyph' };
} else if (new RegExp(/^:.*:$/).test(img)) { // Is a shortcode emoji
return { isEmoji: true, emojiType: 'shortcode' };
} else if (img.substring(0, 2) === 'U+' && img.length === 7) {
return { isEmoji: true, emojiType: 'unicode' };
}
return { isEmoji: false, emojiType: '' };
},
/* Formats and gets emoji from unicode or shortcode */
getEmoji(emojiCode) {
const { emojiType } = this.isEmoji(emojiCode);
if (emojiType === 'shortcode') {
if (emojiLookup[emojiCode]) return emojiLookup[emojiCode];
} else if (emojiType === 'unicode') {
return String.fromCodePoint(parseInt(emojiCode.substr(2), 16));
}
return emojiCode; // Emoji is a glyph already, just return
},
/* Get favicon URL, for items which use the favicon as their icon */
getFavicon(fullUrl) {
const isLocalIP = /(127\.)|(192\.168\.)|(10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(::1$)|([fF][cCdD])|(localhost)/;
if (isLocalIP.test(fullUrl)) { // Check if using a local IP format or localhost
if (this.shouldUseDefaultFavicon(fullUrl)) { // Check if we should use local icon
const urlParts = fullUrl.split('/');
// For locally running services, use the default path for favicon
if (urlParts.length >= 2) return `${urlParts[0]}/${urlParts[1]}/${urlParts[2]}/favicon.ico`;
} else if (fullUrl.includes('http')) {
// For publicly accessible sites, a more reliable method is using Google's API
return `https://s2.googleusercontent.com/s2/favicons?domain=${fullUrl}`;
} else if (fullUrl.includes('http')) { // Service is running publicly
const host = this.getHostName(fullUrl);
const faviconApi = this.config.appConfig.faviconApi || defaultFaviconApi;
const endpoint = faviconApiEndpoints[faviconApi];
return endpoint.replace('$URL', host);
}
return '';
},
/* If using favicon for icon, and if service is running locally (determined by local IP) */
/* or if user prefers local favicon, then return true */
shouldUseDefaultFavicon(fullUrl) {
const isLocalIP = /(127\.)|(192\.168\.)|(10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(::1$)|([fF][cCdD])|(localhost)/;
return (isLocalIP.test(fullUrl) || this.config.appConfig.faviconApi === 'local');
},
getLocalImagePath(img) {
return `/item-icons/${img}`;
},
getGenerativeIcon(url) {
return `https://ipsicon.io/${this.getHostName(url)}.svg`;
},
/* Checks if the icon is from a local image, remote URL, SVG or font-awesome */
getIconPath(img, url) {
switch (this.determineImageType(img)) {
case 'url': return img;
case 'img': return this.getLocalImagePath(img);
case 'favicon': return this.getFavicon(url);
case 'generative': return this.getGenerativeIcon(url);
case 'svg': return img;
case 'emoji': return img;
default: return '';
}
},
@ -84,9 +122,14 @@ export default {
else if (this.isImage(img)) imgType = 'img';
else if (img.includes('fa-')) imgType = 'font-awesome';
else if (img === 'favicon') imgType = 'favicon';
else if (img === 'generative') imgType = 'generative';
else if (this.isEmoji(img).isEmoji) imgType = 'emoji';
else imgType = 'none';
return imgType;
},
getHostName(url) {
try { return new URL(url).hostname; } catch (e) { return url; }
},
/* Called when the path to the image cannot be resolved */
imageNotFound() {
this.broken = true;
@ -98,9 +141,16 @@ export default {
<style lang="scss">
.tile-icon {
width: 60px;
width: 2rem;
// filter: var(--item-icon-transform);
border-radius: var(--curve-factor);
&.broken { display: none; }
&.small {
width: 1.5rem;
}
&.large {
width: 3rem;
}
}
i.fas, i.fab, i.far, i.fal, i.fad {
font-size: 2rem;
@ -120,7 +170,17 @@ export default {
fill: currentColor;
}
}
i.emoji-icon {
font-style: normal;
font-size: 2rem;
margin: 0.2rem;
&.small {
font-size: 1.5rem;
}
&.large {
font-size: 2.5rem;
}
}
.missing-image {
width: 3.5rem;
path {

View File

@ -2,19 +2,24 @@
<div :class="makeClass(position, isSmall, isTransparent)">
<NewTabOpenIcon v-if="openingMethod === 'newtab'" />
<SameTabOpenIcon v-else-if="openingMethod === 'sametab'" />
<IframeOpenIcon v-else-if="openingMethod === 'iframe'" />
<IframeOpenIcon v-else-if="openingMethod === 'modal'" />
<WorkspaceOpenIcon v-else-if="openingMethod === 'workspace'" />
</div>
</template>
<script>
/* This component displays a small icon, indicating opening method */
// Import Icons
import NewTabOpenIcon from '@/assets/interface-icons/open-new-tab.svg';
import SameTabOpenIcon from '@/assets/interface-icons/open-current-tab.svg';
import IframeOpenIcon from '@/assets/interface-icons/open-iframe.svg';
import WorkspaceOpenIcon from '@/assets/interface-icons/open-workspace.svg';
export default {
name: 'ItemOpenMethodIcon',
props: {
openingMethod: String, // newtab | sametab | iframe
openingMethod: String, // newtab | sametab | modal | workspace
isSmall: Boolean, // If true, will apply small class
position: String, // Position classes: top, bottom, left, right
isTransparent: Boolean, // If true, will apply opacity
@ -32,6 +37,7 @@ export default {
NewTabOpenIcon,
SameTabOpenIcon,
IframeOpenIcon,
WorkspaceOpenIcon,
},
};
</script>

View File

@ -0,0 +1,123 @@
<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(--status-check-tooltip-background) !important;
color: var(--status-check-tooltip-color) !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

@ -27,15 +27,21 @@ export default {
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
footer {
width: calc(100% - 0.5rem);
bottom: 0;
padding: 0.25rem;
text-align: center;
color: var(--medium-grey);
opacity: var(--dimming-factor);
background: var(--background-darker);
background: var(--footer-background);
margin-top: 1.5rem;
border-top: 1px solid var(--outline-color);
@include tablet-down {
display: none;
}
}
footer a{

View File

@ -8,10 +8,11 @@
<script>
import PageTitle from '@/components/PageStrcture/PageTitle.vue';
import Nav from '@/components/PageStrcture/Nav.vue';
import { visibleComponents } from '@/utils/defaults';
import { visibleComponents as defaultVisibleComponents } from '@/utils/defaults';
export default {
name: 'Header',
inject: ['visibleComponents'],
components: {
PageTitle,
Nav,
@ -21,9 +22,8 @@ export default {
},
data() {
return {
hiddenComponents: this.pageInfo.hiddenComponents || {},
titleVisible: visibleComponents.pageTitle,
navVisible: visibleComponents.navigation,
titleVisible: (this.visibleComponents || defaultVisibleComponents).pageTitle,
navVisible: (this.visibleComponents || defaultVisibleComponents).navigation,
};
},
};

View File

@ -1,8 +1,8 @@
<template>
<div class="page-titles">
<router-link to="/" class="page-titles">
<h1>{{ title }}</h1>
<span class="subtitle">{{ description }}</span>
</div>
</router-link>
</template>
<script>
@ -21,6 +21,7 @@ export default {
.page-titles {
display: flex;
flex-direction: column;
text-decoration: none;
h1 {
color: var(--heading-text-color);
font-size: 2.5rem;

View File

@ -0,0 +1,62 @@
<template>
<div>
<div class="display-options">
<IconLogout @click="logout()" v-tooltip="tooltip('Sign Out')"
class="layout-icon" tabindex="-2" />
</div>
</div>
</template>
<script>
import { logout as registerLogout } from '@/utils/Auth';
import IconLogout from '@/assets/interface-icons/user-logout.svg';
export default {
name: 'AppButtons',
components: {
IconLogout,
},
methods: {
logout() {
registerLogout();
this.$toasted.show('Logged Out');
setTimeout(() => {
location.reload(true); // eslint-disable-line no-restricted-globals
}, 500);
},
tooltip(content) {
return { content, trigger: 'hover focus', delay: 250 };
},
},
};
</script>
<style scoped lang="scss">
span.options-label {
color: var(--settings-text-color);
}
.display-options {
color: var(--settings-text-color);
svg {
path {
fill: var(--settings-text-color);
}
width: 1rem;
height: 1rem;
margin: 0.2rem;
padding: 0.2rem;
text-align: center;
background: var(--background);
border: 1px solid currentColor;
border-radius: var(--curve-factor);
cursor: pointer;
&:hover, &.selected {
background: var(--settings-text-color);
path { fill: var(--background); }
}
}
}
</style>

View File

@ -10,14 +10,14 @@
</div>
<!-- Modal containing all the configuration options -->
<modal :name="modalNames.CONF_EDITOR" :resizable="true" width="60%" height="80%"
@closed="$emit('modalChanged', false)">
<modal :name="modalNames.CONF_EDITOR" :resizable="true" width="60%" height="85%"
@closed="$emit('modalChanged', false)" classes="dashy-modal">
<ConfigContainer :config="combineConfig()" />
</modal>
<!-- Modal for cloud backup and restore options -->
<modal :name="modalNames.CLOUD_BACKUP" :resizable="true" width="65%" height="60%"
@closed="$emit('modalChanged', false)">
@closed="$emit('modalChanged', false)" classes="dashy-modal">
<CloudBackupRestore :config="combineConfig()" />
</modal>
</div>
@ -100,14 +100,3 @@ export default {
}
}
</style>
<style lang="scss">
.vm--modal {
box-shadow: 0 40px 70px -2px hsl(0deg 0% 0% / 60%), 1px 1px 6px var(--primary);
min-width: 350px;
min-height: 600px;
}
.vm--overlay {
background: #00000080;
}
</style>

View File

@ -83,7 +83,7 @@ export default {
background: var(--search-container-background);
label {
display: inline;
color: var(--settings-text-color);
color: var(--search-label-color);
margin: 0.5rem;
display: inline;
}
@ -105,15 +105,19 @@ export default {
}
}
.clear-search {
position: absolute;
//position: absolute;
color: var(--settings-text-color);
margin: 0.55rem 0 0 -2.2rem;
padding: 0 0.4rem;
font-style: normal;
font-size: 1.5rem;
font-size: 1rem;
opacity: var(--dimming-factor);
border-radius: 50px;
cursor: pointer;
right: 0.5rem;
top: 1rem;
border: 1px solid var(--settings-text-color);
font-size: 1rem;
margin: 0.5rem;
&:hover {
opacity: 1;
background: var(--background-darker);

View File

@ -13,6 +13,7 @@
<ItemSizeSelector :iconSize="iconSize" @iconSizeUpdated="updateIconSize" />
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
@modalChanged="modalChanged" />
<AppButtons v-if="isUserLoggedIn()" />
</div>
<div :class="`show-hide-container ${settingsVisible? 'hide-btn' : 'show-btn'}`">
<button @click="toggleSettingsVisibility()"
@ -23,19 +24,26 @@
</div>
</div>
<KeyboardShortcutInfo />
<AppInfoModal />
</section>
</template>
<script>
import Defaults, { localStorageKeys } from '@/utils/defaults';
import SearchBar from '@/components/Settings/SearchBar';
import ConfigLauncher from '@/components/Settings/ConfigLauncher';
import ThemeSelector from '@/components/Settings/ThemeSelector';
import LayoutSelector from '@/components/Settings/LayoutSelector';
import ItemSizeSelector from '@/components/Settings/ItemSizeSelector';
import AppButtons from '@/components/Settings/AppButtons';
import KeyboardShortcutInfo from '@/components/Settings/KeyboardShortcutInfo';
import AppInfoModal from '@/components/Configuration/AppInfoModal';
import { logout as registerLogout } from '@/utils/Auth';
import IconOpen from '@/assets/interface-icons/config-open-settings.svg';
import IconClose from '@/assets/interface-icons/config-close.svg';
import {
localStorageKeys,
visibleComponents as defaultVisibleComponents,
} from '@/utils/defaults';
export default {
name: 'SettingsContainer',
@ -54,10 +62,13 @@ export default {
ThemeSelector,
LayoutSelector,
ItemSizeSelector,
AppButtons,
KeyboardShortcutInfo,
AppInfoModal,
IconOpen,
IconClose,
},
inject: ['visibleComponents'],
methods: {
userIsTypingSomething(something) {
this.$emit('user-is-searchin', something);
@ -77,6 +88,16 @@ export default {
getInitialTheme() {
return this.appConfig.theme || '';
},
logout() {
registerLogout();
this.$toasted.show('Logged Out');
setTimeout(() => {
location.reload(true); // eslint-disable-line no-restricted-globals
}, 100);
},
isUserLoggedIn() {
return !!localStorage[localStorageKeys.USERNAME];
},
/* Gets user themes if available */
getUserThemes() {
const userThemes = this.appConfig.cssThemes || [];
@ -89,13 +110,13 @@ export default {
},
getSettingsVisibility() {
return JSON.parse(localStorage[localStorageKeys.HIDE_SETTINGS]
|| Defaults.visibleComponents.settings);
|| (this.visibleComponents || defaultVisibleComponents).settings);
},
},
data() {
return {
searchVisible: Defaults.visibleComponents.searchBar,
settingsVisible: this.getSettingsVisibility(),
searchVisible: (this.visibleComponents || defaultVisibleComponents).searchBar,
};
},
};
@ -117,6 +138,7 @@ export default {
position: relative;
flex: 1;
background: var(--settings-background);
border-radius: var(--curve-factor-navbar);
}
.options-container {
display: flex;
@ -177,6 +199,25 @@ export default {
}
}
svg.logout-icon {
path {
fill: var(--settings-text-color);
}
width: 1rem;
height: 1rem;
margin: 0.35rem 0.2rem;
padding: 0.2rem;
text-align: center;
background: var(--background);
border: 1px solid var(--settings-text-color);;
border-radius: var(--curve-factor);
cursor: pointer;
&:hover, &.selected {
background: var(--settings-text-color);
path { fill: var(--background); }
}
}
@include tablet {
section {
display: block;

View File

@ -54,6 +54,7 @@ export default {
}
},
methods: {
/* Sets the theme, by updating data-theme attribute on the html tag */
setLocalTheme(newTheme) {
const htmlTag = document.getElementsByTagName('html')[0];
if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme');

View File

@ -0,0 +1,90 @@
<template>
<nav class="side-bar">
<div v-for="(section, index) in sections" :key="index">
<div @click="openSection(index)" class="side-bar-item-container">
<SideBarItem
class="item"
:icon="section.icon"
:title="section.name"
/>
</div>
<transition name="slide">
<SideBarSection
v-if="isOpen[index]"
:items="section.items"
@launch-app="launchApp"
/>
</transition>
</div>
</nav>
</template>
<script>
import SideBarItem from '@/components/Workspace/SideBarItem.vue';
import SideBarSection from '@/components/Workspace/SideBarSection.vue';
export default {
name: 'SideBar',
inject: ['config'],
props: {
sections: Array,
},
data() {
return {
isOpen: new Array(this.sections.length).fill(false),
};
},
components: {
SideBarItem,
SideBarSection,
},
methods: {
/* Toggles the section clicked, and closes all other sections */
openSection(index) {
this.isOpen = this.isOpen.map((val, ind) => (ind !== index ? false : !val));
},
launchApp(url) {
this.$emit('launch-app', url);
},
},
};
</script>
<style lang="scss" scoped>
@import '@/styles/media-queries.scss';
@import '@/styles/style-helpers.scss';
nav.side-bar {
position: fixed;
display: flex;
flex-direction: column;
background: var(--side-bar-background);
color: var(--side-bar-color);
height: 100%;
width: var(--side-bar-width);
text-align: center;
overflow: auto;
@extend .scroll-bar;
.side-bar-item-container {
z-index: 5;
}
.item:not(:last-child) {
border-bottom: 1px dashed var(--side-bar-color);
z-index: 5;
}
}
.slide-leave-active,
.slide-enter-active {
transition: all 0.1s ease-in-out;
}
.slide-enter {
transform: translate(0, -80%);
}
.slide-leave-to {
transform: translate(0, -80%);
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<div @click="itemClicked()"
:class="`side-bar-item ${icon ? 'w-icon' : 'text-only'}`" v-tooltip="tooltip">
<Icon v-if="icon" :icon="icon" size="small" :url="url" />
<p class="small-title" v-else>{{ title }}</p>
</div>
</template>
<script>
import Icon from '@/components/LinkItems/ItemIcon.vue';
export default {
name: 'SideBarItem',
inject: ['config'],
props: {
icon: String,
title: String,
url: String,
click: Function,
},
components: {
Icon,
},
methods: {
itemClicked() {
if (this.url) this.$emit('launch-app', this.url);
},
},
data() {
return {
tooltip: {
disabled: !this.title,
content: this.title,
trigger: 'hover focus',
hideOnTargetClick: true,
html: false,
placement: 'right-start',
delay: { show: 800, hide: 1000 },
},
};
},
};
</script>
<style lang="scss" scoped>
@import '@/styles/media-queries.scss';
@import '@/styles/style-helpers.scss';
div.side-bar-item {
color: var(--side-bar-color);
background: var(--side-bar-background);
text-align: center;
&.text-only {
background: none;
border: none;
box-shadow: none;
p.small-title {
margin: 0.1rem auto;
font-size: 0.6rem;
}
}
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="sub-side-bar">
<div v-for="(item, index) in items" :key="index">
<SideBarItem
class="item"
:icon="item.icon"
:title="item.title"
:url="item.url"
@launch-app="launchApp"
/>
</div>
</div>
</template>
<script>
import SideBarItem from '@/components/Workspace/SideBarItem.vue';
export default {
name: 'SideBarSection',
inject: ['config'],
props: {
items: Array,
},
components: {
SideBarItem,
},
methods: {
launchApp(url) {
this.$emit('launch-app', url);
},
},
};
</script>
<style lang="scss" scoped>
@import '@/styles/media-queries.scss';
@import '@/styles/style-helpers.scss';
div.sub-side-bar {
display: flex;
flex-direction: column;
background: var(--side-bar-background-lighter);
border-radius: var(--curve-factor);
margin: 0.2rem;
color: var(--side-bar-color);
text-align: center;
z-index: 3;
.item:not(:last-child) {
border-bottom: 1px dashed var(--side-bar-color);
}
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="web-content">
<iframe :src="url" />
</div>
</template>
<script>
export default {
name: 'WebContent',
props: {
url: String,
},
};
</script>
<style lang="scss" scoped>
@import '@/styles/media-queries.scss';
@import '@/styles/style-helpers.scss';
iframe {
position: absolute;
left: var(--side-bar-width);
height: calc(100% - var(--header-height));
width: calc(100% - var(--side-bar-width));
border: none;
background: white;
}
</style>

View File

@ -1,24 +1,31 @@
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 router from './router';
import './registerServiceWorker';
import { toastedOptions } from '@/utils/defaults';
import Dashy from '@/App.vue';
import router from '@/router';
import registerServiceWorker from '@/registerServiceWorker';
import clickOutside from '@/utils/ClickOutside';
Vue.use(VTooltip);
Vue.use(VModal);
Vue.use(VTabs);
Vue.use(Toasted, toastedOptions);
Vue.component('v-select', VSelect);
Vue.directive('clickOutside', clickOutside);
Vue.config.productionTip = false;
// Register Service Worker
registerServiceWorker();
new Vue({
router,
render: (awesome) => awesome(App),
render: (awesome) => awesome(Dashy),
}).$mount('#app');

View File

@ -1,32 +1,88 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker';
import { sessionStorageKeys } from './utils/defaults';
import conf from '../public/conf.yml';
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log(
'App is being served from cache by a service worker.\n'
+ 'For more details, visit https://goo.gl/AFskqB',
);
},
registered() {
console.log('Service worker has been registered.');
},
cached() {
console.log('Content has been cached for offline use.');
},
updatefound() {
console.log('New content is downloading.');
},
updated() {
console.log('New content is available; please refresh.');
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
console.error('Error during service worker registration:', error);
},
});
}
/* Sets a local storage item with the state from the SW lifecycle */
const setSwStatus = (swStateToSet) => {
const initialSwState = {
ready: false,
registered: false,
cached: false,
updateFound: false,
updated: false,
offline: false,
error: false,
devMode: false,
disabledByUser: false,
};
const sessionData = sessionStorage[sessionStorageKeys.SW_STATUS];
const currentSwState = sessionData ? JSON.parse(sessionData) : initialSwState;
try {
const newSwState = { ...currentSwState, ...swStateToSet };
sessionStorage.setItem(sessionStorageKeys.SW_STATUS, JSON.stringify(newSwState));
} catch (e) {
console.warn('Error setting SW data', e);
}
};
/**
* Checks if service workers should be enabled
* Disable if not running in production
* Or disable if user specified to disable
*/
const shouldEnableServiceWorker = () => {
let shouldEnable = true;
if (conf && conf.appConfig) { // Check if app Config available
if (conf.appConfig.disableServiceWorker) { // Disable if user requested
shouldEnable = false;
setSwStatus({ disabledByUser: true });
}
}
if (process.env.NODE_ENV !== 'production') {
shouldEnable = false; // Disable if not in production
setSwStatus({ devMode: true });
}
return shouldEnable;
};
const registerServiceWorker = () => {
if (shouldEnableServiceWorker()) {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
setSwStatus({ ready: true });
console.log(
'App is being served from cache by a service worker.\n'
+ 'For more details, visit https://goo.gl/AFskqB',
);
},
registered() {
setSwStatus({ registered: true });
console.log('Service worker has been registered.');
},
cached() {
setSwStatus({ cached: true });
console.log('Content has been cached for offline use.');
},
updatefound() {
setSwStatus({ updateFound: true });
console.log('New content is downloading.');
},
updated() {
setSwStatus({ updated: true });
console.log('New content is available; please refresh.');
},
offline() {
setSwStatus({ offline: true });
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
setSwStatus({ error: true });
console.error('Error during service worker registration:', error);
},
});
}
};
export default registerServiceWorker;

View File

@ -1,25 +1,20 @@
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import conf from '../public/conf.yml'; // Main site configuration
import { pageInfo as defaultPageInfo, localStorageKeys } from './utils/defaults';
import Home from '@/views/Home.vue';
import Login from '@/views/Login.vue';
import Workspace from '@/views/Workspace.vue';
import DownloadConfig from '@/views/DownloadConfig.vue';
import { isLoggedIn } from '@/utils/Auth';
import { config } from '@/utils/ConfigHelpers';
import { metaTagData } from '@/utils/defaults';
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 isAuthenticated = () => {
const users = config.appConfig.auth;
return (!users || isLoggedIn(users));
};
const router = new Router({
routes: [
@ -27,19 +22,32 @@ const router = new Router({
path: '/',
name: 'home',
component: Home,
props: {
sections: sections || [],
pageInfo: localPageInfo || pageInfo || defaultPageInfo,
appConfig: localAppConfig || appConfig || {},
},
props: config,
meta: {
title: pageInfo.title || 'Home Page',
metaTags: [
{
name: 'description',
content: 'A simple static homepage for you\'re server',
},
],
title: config.pageInfo.title || 'Home Page',
metaTags: metaTagData,
},
},
{
path: '/workspace',
name: 'workspace',
component: Workspace,
props: config,
meta: {
title: config.pageInfo.title || 'Dashy Workspace',
metaTags: metaTagData,
},
},
{
path: '/login',
name: 'login',
component: Login,
props: {
appConfig: config.appConfig,
},
beforeEnter: (to, from, next) => {
if (isAuthenticated()) router.push({ path: '/' });
next();
},
},
{
@ -47,10 +55,25 @@ const router = new Router({
name: 'about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
},
{
path: '/download',
name: 'download',
component: DownloadConfig,
props: config,
meta: {
title: config.pageInfo.title || 'Download Dashy Config',
metaTags: metaTagData,
},
},
],
});
const defaultTitle = 'Speed Dial';
router.beforeEach((to, from, next) => {
if (to.name !== 'login' && !isAuthenticated()) next({ name: 'login' });
else next();
});
const defaultTitle = 'Dashy';
router.afterEach((to) => {
Vue.nextTick(() => {
document.title = to.meta.title || defaultTitle;

View File

@ -31,22 +31,6 @@
--transparent-white-50: #ffffff80;
--transparent-white-30: #ffffff4d;
/* Other Variables */
--outline-color: none;
--curve-factor: 5px; // Border radius for most components
--curve-factor-navbar: 16px; // Border radius for header
--dimming-factor: 0.7; // Opacity for semi-transparent components
/* Settings for specific components */
--scroll-bar-width: 8px;
--item-group-padding: 5px; // Determines width of item-group outline
--item-shadow: 1px 1px 2px #130f23;
--item-hover-shadow: 1px 2px 4px #373737;
--item-icon-transform: drop-shadow(2px 4px 6px var(--transparent-50)) saturate(0.65);
--item-icon-transform-hover: drop-shadow(4px 8px 3px var(--transparent-50)) saturate(2);
--item-group-shadow: var(--item-shadow);
--settings-container-shadow: none;
/* Color variables for specific components
* all variables are optional, since they inherit initial values from above*
* Using specific variables makes overriding for custom themes really easy */
@ -58,6 +42,7 @@
--nav-link-border-color: transparent;
--nav-link-border-color-hover: var(--primary);
--item-text-color: var(--primary);
--item-text-color-hover: var(--item-text-color);
--item-group-outer-background: var(--primary);
--item-group-heading-text-color: var(--item-group-background);
--item-group-heading-text-color-hover: var(--background);
@ -65,8 +50,10 @@
--settings-text-color: var(--primary);
--search-container-background: var(--background-darker);
--search-field-background: var(--background);
--search-label-color: var(--settings-text-color);
--footer-text-color: var(--medium-grey);
--footer-text-color-link: var(--primary);
--footer-background: var(--background-darker);
--welcome-popup-background: var(--background-darker);
--welcome-popup-text-color: var(--primary);
--config-code-background: #fff;
@ -77,7 +64,25 @@
--toast-color: var(--background);
--scroll-bar-color: var(--primary);
--scroll-bar-background: var(--background-darker);
--highlight-color: var(--background);
--highlight-background: var(--primary);
--loading-screen-color: var(--primary);
--loading-screen-background: var(--background);
--login-form-color: var(--primary);
--login-form-background: var(--background);
--login-form-background-secondary: var(--background-darker);
--about-page-color: var(--white);
--about-page-background: var(--background);
--about-page-accent: var(--primary);
--side-bar-background: var(--background-darker);
--side-bar-background-lighter: var(--background);
--side-bar-color: var(--primary);
--status-check-tooltip-background: var(--background-darker);
--status-check-tooltip-color: var(--primary);
--code-editor-color: var(--black);
--code-editor-background: var(--white);
--context-menu-background: var(--background);
--context-menu-color: var(--primary);
--context-menu-secondary-color: var(--background-darker);
}

View File

@ -26,6 +26,7 @@ html[data-theme='thebe'] {
html[data-theme='dracula'] {
--font-headings: 'Allerta Stencil', sans-serif;
--primary: #6272a4;
--background: #44475a;
--background-darker: #282a36;
--item-group-background: #282a36;
@ -34,7 +35,7 @@ html[data-theme='dracula'] {
--item-shadow: none;
--item-hover-shadow: none;
--settings-text-color: #98ace9;
--primary: #6272a4;
--config-settings-color: #98ace9;
.collapsable:nth-child(1n) { background: #8be9fd; .item { border: 1px solid #8be9fd; color: #8be9fd; }}
.collapsable:nth-child(2n) { background: #50fa7b; .item { border: 1px solid #50fa7b; color: #50fa7b; }}
.collapsable:nth-child(3n) { background: #ffb86c; .item { border: 1px solid #ffb86c; color: #ffb86c; }}
@ -86,9 +87,47 @@ html[data-theme='matrix'] {
--curve-factor: 0px;
--font-body: 'Cutive Mono', monospace;
--font-headings: 'VT323', monospace;
--about-page-background: var(--background);
--context-menu-secondary-color: var(--primary);
.prism-editor-wrapper.my-editor {
border: 1px solid var(--primary);
}
div.context-menu ul li:hover {
color: var(--background);
}
}
html[data-theme='blue-purple'] {
--primary: #54dbf8;
--background: #e5e8f5;
--background-darker: #5346f3;
--font-headings: 'Sniglet', cursive;
--dimming-factor: 0.8;
--curve-factor: 6px;
--settings-text-color: var(--background-darker);
--item-text-color: var(--background-darker);
--item-background: var(--white);
--item-background-hover: var(--primary);
--item-group-heading-text-color: var(--background-darker);
--item-group-background: var(--background);
--footer-text-color: var(--white);
--context-menu-background: var(--white);
--context-menu-color: var(--background-darker);
--context-menu-secondary-color: var(--primary);
.item {
box-shadow: none;
border: 1px solid var(--background-darker);
}
section.filter-container form label {
color: var(--primary);
}
footer {
color: var(--white);
}
}
html[data-theme='hacker-girl'] {
@ -148,6 +187,7 @@ html[data-theme='nord-frost'] {
}
html[data-theme='material-original'] {
--font-body: 'Roboto', serif;
--primary: #29B6F6;
--settings-text-color: #01579b;
--background: #e2e1e0;
@ -175,6 +215,18 @@ html[data-theme='material-original'] {
--config-settings-background: #01579b;
--config-settings-color: #fff;
--heading-text-color: #fff;
--status-check-tooltip-background: #f2f2f2;
--status-check-tooltip-color: #01579b;
--login-form-background: #fff;
--about-page-accent: #000;
--about-page-color: var(--background-darker);
--about-page-background: var(--background);
--context-menu-background: var(--white);
--context-menu-secondary-color: var(--white);
div.context-menu ul li:hover {
background: var(--primary);
color: var(--white);
}
}
html[data-theme='material-dark-original'] {
@ -208,9 +260,18 @@ html[data-theme='material-dark-original'] {
--config-settings-color: #41e2ed;
--scroll-bar-color: #08B0BB;
--scroll-bar-background: #131a1f;
--status-check-tooltip-background: #131a1f;
--status-check-tooltip-color: #08B0BB;
&::-webkit-scrollbar-thumb {
border-left: 1px solid #131a1f;
}
div.context-menu {
border: none;
background: #131a1f;
ul li:hover {
background: #333c43;
}
}
}
html[data-theme='colorful'] {
@ -223,15 +284,14 @@ html[data-theme='colorful'] {
--item-group-outer-background: #05070e;
--item-group-heading-text-color: #e8eae1;
--item-group-heading-text-color-hover: #fff;
.item:nth-child(1n) { color: #eb5cad; border: 1px solid #eb5cad; }
.item:nth-child(2n) { color: #985ceb; border: 1px solid #985ceb; }
.item:nth-child(3n) { color: #5c90eb; border: 1px solid #5c90eb; }
.item:nth-child(4n) { color: #5cdfeb; border: 1px solid #5cdfeb; }
.item:nth-child(5n) { color: #5ceb8d; border: 1px solid #5ceb8d; }
.item:nth-child(6n) { color: #afeb5c; border: 1px solid #afeb5c; }
.item:nth-child(7n) { color: #ebb75c; border: 1px solid #ebb75c; }
.item:nth-child(8n) { color: #eb615c; border: 1px solid #eb615c; }
.tile-title span.text { transition: none; }
.item-wrapper:nth-child(1n) { .item { color: #eb5cad; border: 1px solid #eb5cad; } }
.item-wrapper:nth-child(2n) { .item { color: #985ceb; border: 1px solid #985ceb; } }
.item-wrapper:nth-child(3n) { .item { color: #5c90eb; border: 1px solid #5c90eb; } }
.item-wrapper:nth-child(4n) { .item { color: #5cdfeb; border: 1px solid #5cdfeb; } }
.item-wrapper:nth-child(5n) { .item { color: #5ceb8d; border: 1px solid #5ceb8d; } }
.item-wrapper:nth-child(6n) { .item { color: #afeb5c; border: 1px solid #afeb5c; } }
.item-wrapper:nth-child(7n) { .item { color: #ebb75c; border: 1px solid #ebb75c; } }
.item-wrapper:nth-child(8n) { .item { color: #eb615c; border: 1px solid #eb615c; } }
.item:hover, .item:focus {
opacity: 0.85;
outline: none;
@ -243,11 +303,20 @@ html[data-theme='colorful'] {
h1, h2, h3, h4 {
font-weight: normal;
}
div.context-menu {
border-color: var(--primary);
}
}
html[data-theme='minimal-light'], html[data-theme='minimal-dark'] {
--font-body: 'PTMono-Regular', 'Courier New', monospace;
--font-headings: 'PTMono-Regular', 'Courier New', monospace;
html[data-theme='minimal-light'], html[data-theme='minimal-dark'], html[data-theme='vaporware'] {
--font-body: 'Courier New', monospace;
--font-headings: 'Courier New', monospace;
--footer-height: 94px;
.item.size-medium .tile-title {
max-width: 100px;
}
label.lbl-toggle h3 {
font-size: 1.8rem;
}
@ -283,6 +352,8 @@ html[data-theme='material'], html[data-theme='material-dark'] {
--font-headings: 'Francois One', serif;
--curve-factor: 4px;
--curve-factor-navbar: 8px;
--about-page-background: var(--background);
--about-page-color: var(--primary);
.collapsable {
margin: 0;
@ -331,7 +402,7 @@ html[data-theme='material'], html[data-theme='material-dark'] {
}
}
}
.tooltip {
.tooltip.item-description-tooltip {
display: none !important;
}
.orientation-horizontal {
@ -391,9 +462,6 @@ html[data-theme='material'], html[data-theme='material-dark'] {
&:active {
background: #c7c7c754;
}
span.text {
transition: none;
}
&.size-small {
padding-left: 0.5rem;
min-width: 11rem;
@ -438,6 +506,8 @@ html[data-theme='material'] {
--search-container-background: #4285f4;
--welcome-popup-text-color: #f5f5f5;
--footer-text-color: #f5f5f5cc;
// --login-form-background-secondary: #f5f5f5cc;
--context-menu-secondary-color: #f5f5f5;
header {
background: #4285f4;
@ -456,6 +526,14 @@ html[data-theme='material'] {
.prism-editor-wrapper {
background: #f5f5f5;
}
.item:focus {
outline-color: #4285f4cc;
}
div.context-menu {
border: none;
background: var(--white);
ul li:hover { svg path { fill: var(--background-darker); }}
}
}
html[data-theme='material-dark'] {
@ -498,6 +576,10 @@ html[data-theme='material-dark'] {
--config-settings-color: #41e2ed;
--scroll-bar-color: #08B0BB;
--scroll-bar-background: #131a1f;
--status-check-tooltip-color: #131a1f;
// --login-form-color: #131a1f;
--login-form-background-secondary: #131a1f;
&::-webkit-scrollbar-thumb {
border-left: 1px solid #131a1f;
}
@ -506,6 +588,13 @@ html[data-theme='material-dark'] {
background: #131a1f !important;
}
}
div.context-menu {
border: none;
background: var(--background);
ul li:hover {
background: #131a1f;
}
}
}
html[data-theme='minimal-light'] {
@ -527,7 +616,13 @@ html[data-theme='minimal-light'] {
--search-container-background: #fff;
--curve-factor: 4px;
--curve-factor-navbar: 8px;
--status-check-tooltip-background: #f2f2f2;
--status-check-tooltip-color: #000;
--login-form-color: #101931;
--about-page-background: var(--background);
--about-page-color: var(--background-darker);
--context-menu-color: var(--background-darker);
--context-menu-secondary-color: var(--primary);
section.filter-container {
background: #fff;
border-bottom: 1px dashed #00000038;
@ -558,6 +653,8 @@ html[data-theme='minimal-dark'] {
--curve-factor-navbar: 8px;
--item-group-heading-text-color: #fff;
--item-group-heading-text-color-hover: #ffffffbf;
--about-page-background: var(--background);
--about-page-color: var(--primary);
label.lbl-toggle h3 {
font-size: 1.8rem;
@ -570,4 +667,111 @@ html[data-theme='minimal-dark'] {
border: 1px solid #fff;
}
}
div.context-menu {
border-color: var(--primary);
}
}
html[data-theme='vaporware'] {
--primary: #09bfe6;
--background: #100e2c;
--background-darker: #6c27ea;
--background-darker: linear-gradient(0deg, rgba(108,39,234,1) 0%, rgba(132,76,235,1) 80%);
--settings-text-color: #6c27ea;
--item-group-outer-background: #096de6;
--item-group-outer-background: var(--primary);
--item-group-background: #190e2c;
--item-group-heading-text-color: #190e2c;
--item-group-heading-text-color-hover: #5118b9;
--item-text-color: var(--primary);
--item-background: #1a174d;
--item-background-hover: #2b2670;
--footer-text-color: var(--white);
--item-shadow: none;
--curve-factor: 2px;
--curve-factor-navbar: 6px;
--login-form-color: #09bfe6;
--config-settings-background: #100e2c;
.home {
background: linear-gradient(180deg, rgba(16,14,44,1) 10%, rgba(27,24,79,1) 40%, rgba(16,14,44,1) 100%);
}
div.item-group-container {
gap: 0.3rem;
margin: 1rem auto;
}
div.collapsable {
margin: 0.2rem;
padding: 0.2rem;
}
div.content-inner {
padding: 0.15rem !important;
}
a.item {
margin: 0.1rem;
border: 0;
&.size-medium {
min-height: 80px;
}
}
section.filter-container {
background: linear-gradient(0deg, var(--background) 25%, #6c27ea 100%);
form {
background: #6c27ea;
height: 2.5rem;
}
form label, i.clear-search {
color: #100e2c;
border-color: #100e2c;
font-weight: bold;
}
}
.tile-title span.text {
font-weight: normal;
}
label.lbl-toggle h3 {
font-size: 1.4rem;
}
footer {
color: var(--white);
}
div.login-page {
background: url('https://i.ibb.co/JqcJcGK/vaporwave-sunset-wallpaper.jpg');
background-size: cover;
}
// body {
// background: url('https://i.ibb.co/JqcJcGK/vaporwave-sunset-wallpaper.jpg');
// background-size: cover;
// div.home { background: none; }
// }
}
html[data-theme='cyberpunk'] {
--pink: #ff2a6d;
--pale: #d1f7ff;
--aqua: #05d9e8;
--teal: #005678;
--blue: #01012b;
--gold: #ebeb0f;
--primary: var(--gold);
--background: var(--blue);
--background-darker: var(--pink);
--heading-text-color: var(--blue);
--nav-link-background-color-hover: var(--blue);
--nav-link-text-color-hover: var(--pink);
--nav-link-border-color-hover: var(--blue);
--config-settings-background: var(--blue);
--config-settings-color: var(--pink);
--search-label-color: var(--blue);
--item-group-background: var(--blue);
--item-text-color: var(--pale);
--scroll-bar-color: var(--aqua);
--scroll-bar-background: var(--teal);
--footer-background: var(--aqua);
--welcome-popup-background: var(--pink);
--welcome-popup-text-color: var(--blue);
--font-headings: 'Audiowide', cursive;
}

View File

@ -0,0 +1,29 @@
:root {
/* General Variables */
--outline-color: none;
--curve-factor: 5px; // Border radius for most components
--curve-factor-navbar: 16px; // Border radius for header
--curve-factor-small: 2px; // Subtle border radius for util components
--dimming-factor: 0.7; // Opacity for semi-transparent components
/* Basic Page Components */
--scroll-bar-width: 8px;
--header-height: 6.3rem;
--footer-height: 125px;
/* Section & Item dimensions */
--item-group-padding: 5px; // Determines width of item-group outline
--item-shadow: 1px 1px 2px #130f23;
--item-hover-shadow: 1px 2px 4px #373737;
--item-icon-transform: drop-shadow(2px 4px 6px var(--transparent-50)) saturate(0.65);
--item-icon-transform-hover: drop-shadow(4px 8px 3px var(--transparent-50)) saturate(2);
--item-group-shadow: var(--item-shadow);
--context-menu-shadow: var(--item-shadow);
/* Settings and config menu */
--settings-container-shadow: none;
/* Workspace View */
--side-bar-width: 3.5rem; // The width of the sidebar
}

Some files were not shown because too many files have changed in this diff Show More