🔀 Merge pull request #1528 from Lissy93/FEAT/Dashy-V3

[FEAT] Remove the need for rebuild after config changes
This commit is contained in:
Alicia Sykes 2024-04-20 12:07:26 +01:00 committed by GitHub
commit 931915f366
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
97 changed files with 3262 additions and 1788 deletions

99
.env
View File

@ -1,40 +1,59 @@
# Store environmental variables here. All variables are optional.
# Lines beginning in '#' are ignored.
# Can be either development, production or test
# NODE_ENV=production
# The port to expose the running application on
# PORT=4000
# If you've proved SSL certs, then can set HTTPS port
# SSL_PORT=4001
# The host that Dashy is running on, domain or IP
# HOST=localhost
# The default base path for serving up static assets
# BASE_URL=./
# Optionally, specify the path of SSL private + public keys
# SSL_PRIV_KEY_PATH=/etc/ssl/certs/dashy-priv.key
# SSL_PUB_KEY_PATH=/etc/ssl/certs/dashy-pub.pem
# If SSL enabled, choose whether or not to redirect http to https
# Defaults to true
# REDIRECT_HTTPS=true
# Usually the same as BASE_URL, but accessible in frontend
# VUE_APP_DOMAIN=https://dashy.to
# Should enable SRI for build script and link resources
# INTEGRITY=true
# Computed automatically on build. Indicates if running in container
# IS_DOCKER=true
# Again, set automatically using package.json during build time
# VUE_APP_VERSION=2.0.0
# Directory for conf.yml backups
# BACKUP_DIR=./public/
# Store environmental variables here. All variables are optional.
# Lines beginning in '#' are ignored.
# Can be either development, production or test
# NODE_ENV=production
# The port to expose the running application on
# PORT=4000
# If you've proved SSL certs, then can set HTTPS port
# SSL_PORT=4001
# The host that Dashy is running on, domain or IP
# HOST=localhost
# The default base path for serving up static assets
# BASE_URL=./
# Optionally, specify the path of SSL private + public keys
# SSL_PRIV_KEY_PATH=/etc/ssl/certs/dashy-priv.key
# SSL_PUB_KEY_PATH=/etc/ssl/certs/dashy-pub.pem
# If SSL enabled, choose whether or not to redirect http to https
# Defaults to true
# REDIRECT_HTTPS=true
# The path to the user data directory
# USER_DATA_DIR=user-data
# Override where the path to the configuration file is, can be a remote URL
# VUE_APP_CONFIG_PATH=/conf.yml
# Usually the same as BASE_URL, but accessible in frontend
# VUE_APP_DOMAIN=https://dashy.to
# Override the page title for the frontend app
# VUE_APP_TITLE=''
# Set the default view to load on startup (can be `minimal`, `workspace` or `home`)
# VUE_APP_STARTING_VIEW=home
# Set the Vue app routing mode (can be 'hash', 'history' or 'abstract')
# VUE_APP_ROUTING_MODE=history
# Should enable SRI for build script and link resources
# INTEGRITY=true
# Computed automatically on build. Indicates if running in container
# IS_DOCKER=true
# Again, set automatically using package.json during build time
# VUE_APP_VERSION=2.0.0
# Directory for conf.yml backups
# BACKUP_DIR=./user-data/
# Setup any other user defined vars by prepending VUE_APP_ to the var name
# VUE_APP_pihole_ip=http://your.pihole.ip
# VUE_APP_pihole_key=your_pihole_secret_key

4
.github/AUTHORS.txt vendored
View File

@ -163,5 +163,5 @@ Lissy93 <Lissy93@users.noreply.github.com> - 222 commits
Alicia Bot <87835202+liss-bot@users.noreply.github.com> - 240 commits
liss-bot <liss-bot@d0h.co> - 244 commits
Alicia Sykes <gh@d0h.co> - 439 commits
Alicia Sykes <alicia@omg.lol> - 505 commits
Alicia Sykes <sykes.alicia@gmail.com> - 1488 commits
Alicia Sykes <alicia@omg.lol> - 471 commits
Alicia Sykes <sykes.alicia@gmail.com> - 1488 commits

View File

@ -34,6 +34,7 @@ jobs:
bodyFile: ".github/LATEST_CHANGELOG.md"
mark-issue-fixed:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'issues' }}
steps:
- uses: actions/checkout@v2
- name: Label Fixed Issues

View File

@ -1,14 +0,0 @@
# Updates multiple issues with a certain tag, with a comment containing a given message
name: 🎯 Broadcast Message across Issues
on:
workflow_dispatch:
inputs:
message: { required: false }
labels: { required: false }
jobs:
broadcast:
runs-on: ubuntu-latest
steps:
- uses: jenschelkopf/broadcast-action@master
with:
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}

View File

@ -15,4 +15,3 @@ jobs:
collapsibleThreshold: '25'
failOnDowngrade: 'false'
path: 'yarn.lock'
updateComment: 'true'

View File

@ -66,18 +66,3 @@ jobs:
committer_username: liss-bot
committer_email: liss-bot@d0h.co
make-author-list:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: wow-actions/update-authors@v1.1.4
with:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
sort: commits
bots: true
path: .github/AUTHORS.txt
commit: ':blue_heart: Makes author list'
template: '{{name}} <{{email}}> - {{commits}} commits'

View File

@ -10,10 +10,8 @@ jobs:
- uses: apexskier/github-release-commenter@v1
with:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
label-template: 🛩️ Released {release_tag}, 🔨 Fixed
label-template: 🛩️ Released {release_tag}
comment-template: |
**The fix for this issue has now been released in {release_name} ✨**
**This has now been released in {release_name} ✨**
If you haven't done so already, please [update your instance](https://github.com/Lissy93/dashy/blob/master/docs/management.md#updating) to `{release_tag}` or later. See {release_link} for full info.
Feel free to reach out if you need any more support. If you are enjoying Dashy, consider [supporting the project](https://github.com/Lissy93/dashy/blob/master/docs/contributing.md#contributing).

1
.yarnrc.yml Normal file
View File

@ -0,0 +1 @@
nodeLinker: node-modules

View File

@ -28,7 +28,7 @@ RUN yarn build --mode production
FROM node:20.11.1-alpine3.19
# Define some ENV Vars
ENV PORT=80 \
ENV PORT=8080 \
DIRECTORY=/app \
IS_DOCKER=true
@ -40,8 +40,6 @@ RUN apk add --no-cache tzdata
# Copy built application from build phase
COPY --from=BUILD_IMAGE /app ./
# Ensure only one version of conf.yml exists
RUN rm dist/conf.yml
# Finally, run start command to serve up the built application
CMD [ "yarn", "build-and-start" ]

View File

@ -6,27 +6,12 @@
<img width="120" src="https://i.ibb.co/yhbt6CY/dashy.png" />
<br/>
<b><a href="./docs/showcase.md">User Showcase</a></b> | <b><a href="https://demo.dashy.to">Live Demo</a></b> | <b><a href="./docs/quick-start.md">Getting Started</a></b> | <b><a href="https://dashy.to/docs">Documentation</a></b> | <b><a href="https://github.com/Lissy93/dashy">GitHub</a></b>
<br/><br/>
<a href="https://github.com/awesome-selfhosted/awesome-selfhosted#personal-dashboards">
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Self-Hosted">
</a>
<a href="./LICENSE">
<img src="https://img.shields.io/badge/License-MIT-0aa8d2?logo=opensourceinitiative&logoColor=fff" alt="License MIT">
</a>
<a href="./.github/CHANGELOG.md">
<img src="https://img.shields.io/github/package-json/v/lissy93/dashy?logo=azurepipelines&amp;color=0aa8d2" alt="Current Version">
</a>
<a href="https://hub.docker.com/r/lissy93/dashy">
<img src="https://img.shields.io/docker/pulls/lissy93/dashy?logo=docker&color=0aa8d2&logoColor=fff" alt="Docker Pulls">
</a>
<a href="http://as93.link/dashy-build-status">
<img src="https://badgen.net/github/status/lissy93/dashy?icon=github" alt="GitHub Status">
</a>
<a href="https://snyk.io/test/github/lissy93/dashy">
<img src="https://snyk.io/test/github/lissy93/dashy/badge.svg" alt="Known Vulnerabilities">
</a>
</p>
> [!NOTE]
> Version [3.0.0](https://github.com/Lissy93/dashy/releases/tag/3.0.0) has been released, and requires some changes to your setup, see [#1529](https://github.com/Lissy93/dashy/discussions/1529) for details.
<details>
<summary><b>Table of Contents</b></summary>
<p>
@ -95,7 +80,7 @@
**Screenshots**: Checkout the [Showcase](./docs/showcase.md), to see example dashboards from the community
**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) or [`docker run -p 8080:80 lissy93/dashy`](./docs/quick-start.md)
**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) or [`docker run -p 8080:8080 lissy93/dashy`](./docs/quick-start.md)
<p align="center">
@ -116,15 +101,15 @@
You will need [Docker](https://docs.docker.com/get-docker/) installed on your system
```
docker run -p 8080:80 lissy93/dashy
docker run -p 8080:8080 lissy93/dashy
```
Or
```docker
docker run -d \
-p 4000:80 \
-v /root/my-local-conf.yml:/app/public/conf.yml \
-p 4000:8080 \
-v /root/my-local-conf.yml:/app/user-data/conf.yml \
--name my-dashboard \
--restart=always \
lissy93/dashy:latest
@ -140,7 +125,7 @@ See also: [examples with Docker Compose](./docs/deployment.md#using-docker-compo
You will need [git](https://git-scm.com/downloads), the latest or LTS version of [Node.js](https://nodejs.org/) and _(optionally)_ [Yarn](https://yarnpkg.com/) installed on your system.
- Clone the Repo: `git clone https://github.com/Lissy93/dashy.git` and `cd dashy`
- Configuration: Fill in your settings in `./public/conf.yml`
- Configuration: Fill in your settings in `./user-data/conf.yml`
- Install dependencies: `yarn`
- Build: `yarn build`
- Run: `yarn start`
@ -169,7 +154,7 @@ Dashy supports **1-Click deployments** on several popular cloud platforms. To sp
> For full configuration documentation, see: [**Configuring**](./docs/configuring.md)
Dashy is configured through a YAML file, located at `./public/conf.yml`. In addition, you can find a complete list of available options in the [Configuring Docs](./docs/configuring.md). The config can also be edited and saved directly through the UI.
Dashy is configured through a YAML file, located at `./user-data/conf.yml`. In addition, you can find a complete list of available options in the [Configuring Docs](./docs/configuring.md). The config can also be edited and saved directly through the UI.
**[⬆️ Back to Top](#dashy)**
@ -581,7 +566,8 @@ Huge thanks to the sponsors helping to support Dashy's development!
<br />
<sub><b>Shrippen</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/bile0026">
<img src="https://avatars.githubusercontent.com/u/5022496?u=aec96ad173c0ea9baaba93807efa8a848af6595c&v=4" width="80;" alt="bile0026"/>
@ -843,16 +829,25 @@ For more info, see TLDR Legal's [Explanation of MIT](https://tldrlegal.com/licen
---
<p align="center">
<br>
<a href="https://dashboard.trackgit.com/token/ks0bx7bb14lsvbwoc3ik">
<img src="https://us-central1-trackgit-analytics.cloudfunctions.net/token/ping/ks0bx7bb14lsvbwoc3ik?style=flat-square" />
</a>
<br><br>
<a href="https://github.com/Lissy93/dashy">
<img src="https://github.githubassets.com/images/icons/emoji/octocat.png" />
</a>
<br><br>
<i>Thank you for Visiting</i>
<!-- License + Copyright -->
<p align="center">
<i>© <a href="https://aliciasykes.com">Alicia Sykes</a> 2024</i><br>
<i>Licensed under <a href="https://gist.github.com/Lissy93/143d2ee01ccc5c052a17">MIT</a></i><br>
<a href="https://github.com/lissy93"><img src="https://i.ibb.co/4KtpYxb/octocat-clean-mini.png" /></a><br>
<sup>Thanks for visiting :)</sup>
</p>
<!-- Dinosaurs are Awesome -->
<!--
. - ~ ~ ~ - .
.. _ .-~ ~-.
//| \ `..~ `.
|| | } } / \ \
(\ \\ \~^..' | } \
\`.-~ o / } | / \
(__ | / | / `.
`- - ~ ~ -._| /_ - ~ ~ ^| /- _ `.
| / | / ~-. ~- _
|_____| |_____| ~ - . _ _~_-_
-->

View File

@ -12,21 +12,17 @@ services:
# To build from source, replace 'image: lissy93/dashy' with 'build: .'
# build: .
# Or, to use a Dockerfile for your archtecture, uncomment the following
# context: .
# dockerfile: ./docker/Dockerfile-arm32v7
# You can also use an image with a different tag, or pull from a different registry, e.g:
# image: ghcr.io/lissy93/dashy or image: lissy93/dashy:arm64v8
# image: ghcr.io/lissy93/dashy or image: lissy93/dashy:3.0.0
# Pass in your config file below, by specifying the path on your host machine
# volumes:
# - /path/to/my-config.yml:/app/public/conf.yml
# - /path/to/item-icons:/app/public/item-icons
# - /path/to/my-config.yml:/app/user-data/conf.yml
# - /path/to/item-icons:/app/user-data/item-icons/
# Set port that web service will be served on. Keep container port as 80
ports:
- 4000:80
- 4000:8080
# Set any environmental variables
environment:

View File

@ -55,7 +55,7 @@
**Screenshots**: Checkout the [Showcase](https://github.com/Lissy93/dashy/blob/master/docs/showcase.md), to see example dashboards from the community
**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) or [`docker run -p 8080:80 lissy93/dashy`](./docs/quick-start.md)
**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) or [`docker run -p 8080:8080 lissy93/dashy`](./docs/quick-start.md)
<p align="center">
@ -69,7 +69,7 @@
## Getting Started 🛫
To deploy Dashy with Docker, just run `docker run -p 8080:80 lissy93/dashy`, then open `http://localhost:8080`
To deploy Dashy with Docker, just run `docker run -p 8080:8080 lissy93/dashy`, then open `http://localhost:8080`
For full list of options and a Docker compose file, see the [Deployment Docs](https://github.com/Lissy93/dashy/blob/master/docs/deployment.md).

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 MiB

After

Width:  |  Height:  |  Size: 29 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -20,7 +20,7 @@
> [!IMPORTANT]
> Dashy's built-in auth is not indented to protect a publicly hosted instance against unauthorized access. Instead you should use an auth provider compatible with your reverse proxy, or access Dashy via your VPN.
> Dashy's built-in auth is not indented to protect a publicly hosted instance against unauthorized access. Instead you should use an auth provider compatible with your reverse proxy, or access Dashy via your VPN, or implement your own SSO logic.
>
> In cases where Dashy is only accessibly within your home network, and you just want to add a login page, then the built-in auth may be sufficient, but keep in mind that configuration can still be accessed.
@ -28,6 +28,11 @@
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 the homepage will resolve to your dashboard.
> [!NOTE]
> Since the auth is initiated in the main app entry point (for security), a rebuild is required to apply changes to the auth configuration.
> You can trigger a rebuild through the UI, under Config --> Rebuild, or by running `yarn build` in the root directory.
### 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.
@ -263,7 +268,7 @@ In NGINX you can specify [control access](https://docs.nginx.com/nginx/admin-gui
```text
server {
listen 80;
listen 8080;
server_name www.dashy.example.com;
location / {
root /path/to/dashy/;

View File

@ -1,5 +1,7 @@
# Cloud Backup and Restore
Beyond the cloud backup/restore service, there are several other self-hosted options you can use to backup Dashy, and any other Docker container data. These are outlined in the Management docs, at: [Docker Backup Options](/docs/management.md#backing-up).
Dashy has a 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.
This is useful not only for backing up your configuration off-site, but it also enables Dashy to be used without having write a YAML config file, and makes it possible to use a public hosted instance, without the need to self-host.

View File

@ -1,6 +1,6 @@
# 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 using Docker, this file can be passed in as a volume. Changes can either be made directly to this file, or done [through the UI](#editing-config-through-the-ui). From the UI you can also export, backup, reset, validate and download your configuration file.
All app configuration is specified in [`/user-data/conf.yml`](https://github.com/Lissy93/dashy/blob/master/user-data/conf.yml) which is in [YAML Format](https://yaml.org/) format. If you're using Docker, this file can be passed in as a volume. Changes can either be made directly to this file, or done [through the UI](#editing-config-through-the-ui). From the UI you can also export, backup, reset, validate and download your configuration file.
## There are three ways to edit the config
@ -36,6 +36,7 @@ The following file provides a reference of all supported configuration options.
- [`auth`](#appconfigauth-optional) - Built-in authentication setup
- [`users`](#appconfigauthusers-optional) - List or users (for simple auth)
- [`keycloak`](#appconfigauthkeycloak-optional) - Auth config for Keycloak
- [`headerAuth`](#appconfigauthheaderauth-optional) - Auth config for HeaderAuth
- [**`sections`**](#section) - List of sections
- [`displayData`](#sectiondisplaydata-optional) - Section display settings
- [`show/hideForKeycloakUsers`](#sectiondisplaydatahideforkeycloakusers-sectiondisplaydatashowforkeycloakusers-itemdisplaydatahideforkeycloakusers-and-itemdisplaydatashowforkeycloakusers) - Set user controls
@ -101,7 +102,7 @@ The following file provides a reference of all supported configuration options.
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`language`** | `string` | _Optional_ | The 2 (or 4-digit) [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language, e.g. `en` or `en-GB`. This must be a language that the app has already been [translated](https://github.com/Lissy93/dashy/tree/master/src/assets/locales) into. If your language is unavailable, Dashy will fallback to English. By default Dashy will attempt to auto-detect your language, although this may not work on some privacy browsers.
**`startingView`** | `enum` | _Optional_ | Which page to load by default, and on the base page or domain root. You can still switch to different views from within the UI. Can be either `default`, `minimal` or `workspace`. Defaults to `default`
~~**`startingView`**~~ | `enum` | _Optional_ | Which page to load by default, and on the base page or domain root. You can still switch to different views from within the UI. Can be either `default`, `minimal` or `workspace`. Defaults to `default`. NOTE: This has been replaced by an environmental variable: `VUE_APP_STARTING_VIEW` in V3 onwards
**`defaultOpeningMethod`** | `enum` | _Optional_ | The default opening method for items, if no `target` is specified for a given item. Can be either `newtab`, `sametab`, `modal`, `workspace`, `clipboard`, `top` or `parent`. Defaults to `newtab`
**`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`** | `number` | _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`
@ -142,11 +143,21 @@ The following file provides a reference of all supported configuration options.
## `appConfig.auth` _(optional)_
> [!NOTE]
> Since the auth is initiated in the main app entry point (for security), a rebuild is required to apply changes to the auth configuration.
> You can trigger a rebuild through the UI, under Config --> Rebuild, or by running `yarn build` in the root directory.
> [!WARNING]
> Built-in auth should **not be used** for security-critical applications, or if your Dashy instance is publicly accessible.
> For these, it is recommended to use an [alternate authentication method](/docs/authentication.md#alternative-authentication-methods).
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`users`** | `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. See [`appConfig.auth.users`](#appconfigauthusers-optional). <br>**Note** this method of authentication is handled on the client side, so for security critical situations, it is recommended to use an [alternate authentication method](/docs/authentication.md#alternative-authentication-methods).
**`enableKeycloak`** | `boolean` | _Optional_ | If set to `true`, then authentication using Keycloak will be enabled. Note that you need to have an instance running, and have also configured `auth.keycloak`. Defaults to `false`
**`keycloak`** | `object` | _Optional_ | Config options to point Dashy to your Keycloak server. Requires `enableKeycloak: true`. See [`auth.keycloak`](#appconfigauthkeycloak-optional) for more info
**`enableHeaderAuth`** | `boolean` | _Optional_ | If set to `true`, then authentication using HeaderAuth will be enabled. Note that you need to have your web server/reverse proxy running, and have also configured `auth.headerAuth`. Defaults to `false`
**`headerAuth`** | `object` | _Optional_ | Config options to point Dashy to your headers for authentication. Requires `enableHeaderAuth: true`. See [`auth.headerAuth`](#appconfigauthheaderauth-optional) for more info
**`enableGuestAccess`** | `boolean` | _Optional_ | When set to `true`, an unauthenticated user will be able to access the dashboard, with read-only access, without having to login. Requires `auth.users` to be configured. Defaults to `false`.
For more info, see the **[Authentication Docs](/docs/authentication.md)**
@ -174,6 +185,15 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**[⬆️ Back to Top](#configuring)**
## `appConfig.auth.headerAuth` _(optional)_
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`userHeader`** | `string` | _Optional_ | The Header name which contains username (default: REMOTE_USER). Case insensitive
**`proxyWhitelist`** | `array` | Required | An array of Upstream proxy servers to expect authencticated requests from
**[⬆️ Back to Top](#configuring)**
## `appConfig.webSearch` _(optional)_
**Field** | **Type** | **Required**| **Description**

View File

@ -7,7 +7,7 @@ Welcome to Dashy, so glad you're here :) Deployment is super easy, and there are
If you want to skip the fuss, and [get straight down to it](/docs/quick-start.md), then you can spin up a new instance of Dashy by running:
```bash
docker run -p 8080:80 lissy93/dashy
docker run -p 8080:8080 lissy93/dashy
```
See [Management Docs](/docs/management.md) for info about securing, monitoring, updating, health checks, auto starting, web server configuration, etc
@ -67,8 +67,8 @@ Dashy has a built container image hosted on [Docker Hub](https://hub.docker.com/
```bash
docker run -d \
-p 8080:80 \
-v /root/my-local-conf.yml:/app/public/conf.yml \
-p 8080:8080 \
-v /root/my-local-conf.yml:/app/user-data/conf.yml \
--name my-dashboard \
--restart=always \
lissy93/dashy:latest
@ -110,9 +110,9 @@ services:
container_name: Dashy
# Pass in your config file below, by specifying the path on your host machine
# volumes:
# - /root/my-config.yml:/app/public/conf.yml
# - /root/my-config.yml:/app/user-data/conf.yml
ports:
- 4000:80
- 4000:8080
# Set any environmental variables
environment:
- NODE_ENV=production
@ -166,8 +166,8 @@ Installing dashy is really simply and fast:
```bash
docker run -d \
-p 4000:80 \
-v /volume1/docker/dashy/my-local-conf.yml:/app/public/conf.yml \
-p 4000:8080 \
-v /volume1/docker/dashy/my-local-conf.yml:/app/user-data/conf.yml \
--name dashy \
--restart=always \
lissy93/dashy:latest
@ -182,7 +182,7 @@ dashy should be up within 1-2min after you've started the install task procedure
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, and optionally [yarn](https://yarnpkg.com/)
1. Get Code: `git clone https://github.com/Lissy93/dashy.git` and `cd dashy`
2. Configuration: Fill in you're settings in `./public/conf.yml`
2. Configuration: Fill in you're settings in `./user-data/conf.yml`
3. Install dependencies: `yarn`
4. Build: `yarn build`
5. Run: `yarn start`

View File

@ -51,7 +51,7 @@ Dashy should now be being served on <http://localhost:8080/>. Hot reload is enab
#### Utils and Checks
- **`yarn validate-config`** - If you have quite a long configuration file, you may wish to check that it's all good to go, before deploying the app. This can be done with `yarn validate-config` or `docker exec -it [container-id] yarn validate-config`. Your config file needs to be in `/public/conf.yml` (or within your Docker container at `/app/public/conf.yml`). This will first check that your YAML is valid, and then validates it against Dashy's [schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js).
- **`yarn validate-config`** - If you have quite a long configuration file, you may wish to check that it's all good to go, before deploying the app. This can be done with `yarn validate-config` or `docker exec -it [container-id] yarn validate-config`. Your config file needs to be in `/user-data/conf.yml` (or within your Docker container at `/app/user-data/conf.yml`). This will first check that your YAML is valid, and then validates it against Dashy's [schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js).
- **`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
#### Alternate Start Commands

View File

@ -104,7 +104,7 @@ If you are not comfortable with making pull requests, or do not want to modify t
This section is for, adding a new setting to the config file.
All of the users config is specified in `./public/conf.yml` - see [Configuring Docs](./configuring.md) for info.
All of the users config is specified in `./user-data/conf.yml` - see [Configuring Docs](./configuring.md) for info.
It's important to first ensure that there isn't a similar option already available, the new option is definitely necessary, and most importantly that it is fully backwards compatible.
Next choose the appropriate section to place it under

View File

@ -167,7 +167,7 @@ You can also set an icon by passing in a valid URL pointing to the icons locatio
## 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 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 `./user-data/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/user-data/item-icons/`. To reference an icon stored locally, just specify it's name and extension. For example, if my icon was stored in `/app/user-data/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 organized. You would then specify an icon with it's folder name slash image name. For example: `networking/monit.png`
@ -187,7 +187,7 @@ If you don't wish for a given item or section to have an icon, just leave out th
## Icon Collections and Resources
The following websites provide good-quality, free icon sets. To use any of these icons, either copy the link to the raw icon (it should end in `.svg` or `.png`) and paste it as your `icon`, or download and save the icons in `/public/item-icons` / mapped Docker volume. Full credit to the authors, please see the licenses for each service for usage and copyright information.
The following websites provide good-quality, free icon sets. To use any of these icons, either copy the link to the raw icon (it should end in `.svg` or `.png`) and paste it as your `icon`, or download and save the icons in `/user-data/item-icons` / mapped Docker volume. Full credit to the authors, please see the licenses for each service for usage and copyright information.
- [Icons for Self-Hosted Apps](https://thehomelab.wiki/books/helpful-tools-resources/page/icons-for-self-hosted-dashboards) - 350+ high-quality icons for commonly self-hosted services
- [SVG Box](https://svgbox.net/iconsets/) - Cryptocurrency, social media apps and flag icons

View File

@ -30,11 +30,11 @@ _The following article is a primer on managing self-hosted apps. It covers every
Although not essential, you will most likely want to provide several assets to your running app.
This is easy to do using [Docker Volumes](https://docs.docker.com/storage/volumes/), which lets you share a file or directory between your host system, and the container. Volumes are specified in the Docker run command, or Docker compose file, using the `--volume` or `-v` flags. The value of which consists of the path to the file / directory on your host system, followed by the destination path within the container. Fields are separated by a colon (`:`), and must be in the correct order. For example: `-v ~/alicia/my-local-conf.yml:/app/public/conf.yml`
This is easy to do using [Docker Volumes](https://docs.docker.com/storage/volumes/), which lets you share a file or directory between your host system, and the container. Volumes are specified in the Docker run command, or Docker compose file, using the `--volume` or `-v` flags. The value of which consists of the path to the file / directory on your host system, followed by the destination path within the container. Fields are separated by a colon (`:`), and must be in the correct order. For example: `-v ~/alicia/my-local-conf.yml:/app/user-data/conf.yml`
In Dashy, commonly configured resources include:
- `./public/conf.yml` - Your main application config file
- `./user-data/conf.yml` - Your main application config file
- `./public/item-icons` - A directory containing your own icons. This allows for offline access, and better performance than fetching from a CDN
- Also within `./public` you'll find standard website assets, including `favicon.ico`, `manifest.json`, `robots.txt`, etc. There's no need to pass these in, but you can do so if you wish
- `/src/styles/user-defined-themes.scss` - A stylesheet for applying custom CSS to your app. You can also write your own themes here.
@ -197,7 +197,9 @@ docker run --rm -v some_volume:/volume -v /tmp:/backup alpine sh -c "rm -rf /vol
### Dashy-Specific Backup
Since Dashy is open source, and freely available, providing you're configuration data is passed in as volumes, there shouldn't be any need to backup the main container. Your main config file, and any assets you're using should be kept backed up, preferably in at least two places, and you should ensure that you can easily restore from backup, if needed.
All configuration and dashboard settings are stored in your `user-data/conf.yml` file. If you provide additional assets (like icons, fonts, themes, etc), these will also live in the `user-data` directory. So to backup all Dashy data, this is the only directory you need to backup.
Since Dashy is open source, there shouldn't be any need to backup the main container.
Dashy also has a built-in cloud backup feature, which is free for personal users, and will let you make and restore fully encrypted backups of your config directly through the UI. To learn more, see the [Cloud Backup Docs](/docs/backup-restore.md)
@ -238,7 +240,7 @@ Once you've generated your SSL cert, you'll need to pass it to Dashy. This can b
```bash
docker run -d \
-p 8080:80 \
-p 8080:8080 \
-v ~/my-private-key.key:/etc/ssl/certs/dashy-priv.key:ro \
-v ~/my-public-key.pem:/etc/ssl/certs/dashy-pub.pem:ro \
lissy93/dashy:latest
@ -276,9 +278,9 @@ services:
container_name: Dashy
image: lissy93/dashy
volumes:
- /root/my-config.yml:/app/public/conf.yml
- /root/my-config.yml:/app/user-data/conf.yml
ports:
- 4000:80
- 4000:8080
environment:
- BASE_URL=/my-dashboard
restart: unless-stopped
@ -550,7 +552,7 @@ upstream dashy {
}
server {
listen 80;
listen 8080;
server_name dashy.mydomain.com;
# Setup SSL
@ -577,7 +579,7 @@ Similarly, a basic `Caddyfile` might look like:
```text
dashy.example.com {
reverse_proxy / nginx:80
reverse_proxy / nginx:8080
}
```
@ -614,7 +616,7 @@ To prevent known container escape vulnerabilities, which typically end in escala
Docker enables you to limit resource consumption (CPU, memory, disk) on a per-container basis. This not only enhances system performance, but also prevents a compromised container from consuming a large amount of resources, in order to disrupt service or perform malicious activities. To learn more, see the [Resource Constraints Docs](https://docs.docker.com/config/containers/resource_constraints/)
For example, to run Dashy with max of 1GB ram, and max of 50% of 1 CP core:
`docker run -d -p 8080:80 --cpus=".5" --memory="1024m" lissy93/dashy:latest`
`docker run -d -p 8080:8080 --cpus=".5" --memory="1024m" lissy93/dashy:latest`
### Don't Run as Root
@ -629,7 +631,7 @@ One of the best ways to prevent privilege escalation attacks, is to configure th
You can specify a user, using the [`--user` param](https://docs.docker.com/engine/reference/run/#user), and should include the user ID (`UID`), which can be found by running `id -u`, and the and the group ID (`GID`), using `id -g`.
With Docker run, you specify it like:
`docker run --user 1000:1000 -p 8080:80 lissy93/dashy`
`docker run --user 1000:1000 -p 8080:8080 lissy93/dashy`
Of if you're using Docker-compose, you could use an environmental variable
@ -639,7 +641,7 @@ services:
dashy:
image: lissy93/dashy
user: ${CURRENT_UID}
ports: [ 4000:80 ]
ports: [ 4000:8080 ]
```
And then to set the variable, and start the container, run: `CURRENT_UID=$(id -u):$(id -g) docker-compose up`
@ -659,7 +661,7 @@ version: "3.8"
services:
dashy:
image: lissy93/dashy
ports: [ 4000:80 ]
ports: [ 4000:8080 ]
cap_drop:
- ALL
cap_add:
@ -675,7 +677,7 @@ services:
To prevent processes inside the container from getting additional privileges, pass in the `--security-opt=no-new-privileges:true` option to the Docker run command (see [docs](https://docs.docker.com/engine/reference/run/#security-configuration)).
Run Command:
`docker run --security-opt=no-new-privileges:true -p 8080:80 lissy93/dashy`
`docker run --security-opt=no-new-privileges:true -p 8080:8080 lissy93/dashy`
Docker Compose
@ -701,14 +703,14 @@ You can specify that a specific volume should be read-only by appending `:ro` to
```bash
docker run -d \
-p 8080:80 \
-v ~/dashy-conf.yml:/app/public/conf.yml \
-p 8080:8080 \
-v ~/dashy-conf.yml:/app/user-data/conf.yml \
-v ~/dashy-icons:/app/public/item-icons:ro \
-v ~/dashy-theme.scss:/app/src/styles/user-defined-themes.scss:ro \
lissy93/dashy:latest
```
You can also prevent a container from writing any changes to volumes on your host's disk, using the `--read-only` flag. Although, for Dashy, you will not be able to write config changes to disk, when edited through the UI with this method. You could make this work, by specifying the config directory as a temp write location, with `--tmpfs /app/public/conf.yml` - but that this will not write the volume back to your host.
You can also prevent a container from writing any changes to volumes on your host's disk, using the `--read-only` flag. Although, for Dashy, you will not be able to write config changes to disk, when edited through the UI with this method. You could make this work, by specifying the config directory as a temp write location, with `--tmpfs /app/user-data/conf.yml` - but that this will not write the volume back to your host.
### Set the Logging Level
@ -778,8 +780,8 @@ Create a new file in `/etc/nginx/sites-enabled/dashy`
```text
server {
listen 80;
listen [::]:80;
listen 8080;
listen [::]:8080;
root /var/www/dashy/html;
index index.html;
@ -898,7 +900,7 @@ Similar to above, you'll first need to fork and clone Dashy to your local system
Then, either use Dashy's default [`Dockerfile`](https://github.com/Lissy93/dashy/blob/master/Dockerfile) as is, or modify it according to your needs.
To build and deploy locally, first build the app with: `docker build -t dashy .`, and then start the app with `docker run -p 8080:80 --name my-dashboard dashy`. Or modify the `docker-compose.yml` file, replacing `image: lissy93/dashy` with `build: .` and run `docker compose up`.
To build and deploy locally, first build the app with: `docker build -t dashy .`, and then start the app with `docker run -p 8080:8080 --name my-dashboard dashy`. Or modify the `docker-compose.yml` file, replacing `image: lissy93/dashy` with `build: .` and run `docker compose up`.
Your container should now be running, and will appear in the list when you run `docker container ls a`. If you'd like to enter the container, run `docker exec -it [container-id] /bin/ash`.

View File

@ -192,7 +192,7 @@ The following section outlines all data that is stored in the browsers, as cooki
> [Local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) is persisted between sessions, and only deleted when manually removed
- `LANGUAGE` - The locale to show app text in
- `HIDE_WELCOME_BANNER` - Set to true once user dismissed welcome message, so that it's not shown again
- `HIDE_INFO_NOTIFICATION` - Set to true once user dismissed welcome message, so that it's not shown again
- `LAYOUT_ORIENTATION` - Preferred section layout, either horizontal, vertical or auto
- `COLLAPSE_STATE` - Remembers which sections are collapsed
- `ICON_SIZE` - Size of items, either small, medium or large

View File

@ -2,7 +2,7 @@
Welcome to Dashy! So glad you're here 😊 In a couple of minutes, you'll have your new dashboard up and running 🚀
**TLDR;** Run `docker run -p 8080:80 lissy93/dashy`, then open `http://localhost:8080`
**TLDR;** Run `docker run -p 8080:8080 lissy93/dashy`, then open `http://localhost:8080`
---
@ -19,8 +19,8 @@ To pull the latest image, and build and start the app run:
```bash
docker run -d \
-p 8080:80 \
-v ~/my-conf.yml:/app/public/conf.yml \
-p 8080:8080 \
-v ~/my-conf.yml:/app/user-data/conf.yml \
--name my-dashboard \
--restart=always \
lissy93/dashy:latest
@ -35,7 +35,7 @@ Your dashboard should now be up and running at `http://localhost:8080` (or your
## 3. Configure
Now that you've got Dashy running, you are going to want to set it up with your own content.
Config is written in [YAML Format](https://yaml.org/), and saved in [`/public/conf.yml`](https://github.com/Lissy93/dashy/blob/master/public/conf.yml).
Config is written in [YAML Format](https://yaml.org/), and saved in [`/user-data/conf.yml`](https://github.com/Lissy93/dashy/blob/master/user-data/conf.yml).
The format on the config file is pretty straight forward. There are three root attributes:
- [`pageInfo`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfo) - Dashboard meta data, like title, description, nav bar links and footer text
@ -72,7 +72,7 @@ sections: # An array of sections
Notes:
- You can use a Docker volume to pass a config file from your host system to the container
- E.g. `-v ./host-system/my-local-conf.yml:/app/public/conf.yml`
- E.g. `-v ./host-system/my-local-conf.yml:/app/user-data/conf.yml`
- It's also possible to edit your config directly through the UI, and changes will be saved in this file
- Check your config against Dashy's schema, with `docker exec -it [container-id] yarn validate-config`
- You might find it helpful to look at some examples, a collection of which can be [found here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10)
@ -118,7 +118,7 @@ yarn build # Build the app
yarn start # Start the app
```
Then edit `./public/conf.yml` and rebuild the app with `yarn build`
Then edit `./user-data/conf.yml` and rebuild the app with `yarn build`
---
@ -129,7 +129,7 @@ Don't have a server? No problem! You can run Dashy for free on Netlify (as well
1. Fork Dashy's repository on GitHub
2. [Log in](app.netlify.com/login/) to Netlify with GitHub
3. Click "New site from Git" and select your forked repo, then click **Deploy**!
4. You can then edit the config in `./public/conf.yml` in your repo, and Netlify will rebuild the app
4. You can then edit the config in `./user-data/conf.yml` in your repo, and Netlify will rebuild the app
---

View File

@ -156,7 +156,7 @@ If you're getting an error about scenarios, then you've likely installed the wro
Alternatively, as a workaround, you have several options:
- Try using [NPM](https://www.npmjs.com/get-npm) instead: So clone, cd, then run `npm install`, `npm run build` and `npm start`
- Try using [Docker](https://www.docker.com/get-started) instead, and all of the system setup and dependencies will already be taken care of. So from within the directory, just run `docker build -t lissy93/dashy .` to build, and then use docker start to run the project, e.g: `docker run -it -p 8080:80 lissy93/dashy` (see the [deploying docs](https://github.com/Lissy93/dashy/blob/master/docs/deployment.md#deploy-with-docker) for more info)
- Try using [Docker](https://www.docker.com/get-started) instead, and all of the system setup and dependencies will already be taken care of. So from within the directory, just run `docker build -t lissy93/dashy .` to build, and then use docker start to run the project, e.g: `docker run -it -p 8080:8080 lissy93/dashy` (see the [deploying docs](https://github.com/Lissy93/dashy/blob/master/docs/deployment.md#deploy-with-docker) for more info)
---
@ -234,7 +234,7 @@ Version 2.0.4 introduced changes to how the config is read, and the app is build
```yaml
volumes:
- /srv/dashy/conf.yml:/app/public/conf.yml
- /srv/dashy/conf.yml:/app/user-data/conf.yml
- /srv/dashy/item-icons:/app/public/item-icons
```
@ -273,12 +273,12 @@ See also: #479, #409, #507, #491, #341, #520
Error response from daemon: OCI runtime create failed: container_linux.go:380:
starting container process caused: process_linux.go:545: container init caused:
rootfs_linux.go:76: mounting "/home/ubuntu/my-conf.yml" to rootfs at
"/app/public/conf.yml" caused: mount through procfd: not a directory:
"/app/user-data/conf.yml" caused: mount through procfd: not a directory:
unknown: Are you trying to mount a directory onto a file (or vice-versa)?
Check if the specified host path exists and is the expected type.
```
If you get an error similar to the one above, you are mounting a directory to the config file's location, when a plain file is expected. Create a YAML file, (`touch my-conf.yml`), populate it with a sample config, then pass it as a volume: `-v ./my-local-conf.yml:/app/public/conf.yml`
If you get an error similar to the one above, you are mounting a directory to the config file's location, when a plain file is expected. Create a YAML file, (`touch my-conf.yml`), populate it with a sample config, then pass it as a volume: `-v ./my-local-conf.yml:/app/user-data/conf.yml`
---

View File

@ -92,6 +92,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [Widget Usage Guide](#widget-usage-guide)
- [Continuous Updates](#continuous-updates)
- [Proxying Requests](#proxying-requests)
- [Handling Secrets](#handling-secrets)
- [Setting Timeout](#setting-timeout)
- [Adding Labels](#adding-labels)
- [Ignoring Errors](#ignoring-errors)
@ -1554,6 +1555,19 @@ Displays the number of queries blocked by [Pi-Hole](https://pi-hole.net/).
apiKey: xxxxxxxxxxxxxxxxxxxxxxx
```
> [!TIP]
> In order to avoid leaking secret data, both `hostname` and `apiKey` can leverage environment variables. Simply pass the name of the variable, which MUST start with `VUE_APP_`.
```yaml
- type: pi-hole-stats
options:
hostname: VUE_APP_pihole_ip
apiKey: VUE_APP_pihole_key
```
> [!IMPORTANT]
> You will need to restart the server (or the docker image) if adding/editing an env var for this to be refreshed.
#### Info
- **CORS**: 🟢 Enabled
@ -2843,6 +2857,32 @@ Vary: Origin
---
### Handling Secrets
Some widgets require you to pass potentially sensetive info such as API keys. The `conf.yml` is not ideal for this, as it's stored in plaintext.
Instead, for secrets you should use environmental vairables.
You can do this, by setting the environmental variable name as the value, instead of the actual key, and then setting that env var in your container or local environment.
The key can be named whatever you like, but it must start with `VUE_APP_` (to be picked up by Vue). If you need to update any of these values, a rebuild is required (this can be done under the Config menu in the UI, or by running `yarn build` then restarting the container).
For more infomation about setting and managing your environmental variables, see [Management Docs --> Environmental Variables](/docs/management.md#passing-in-environmental-variables).
For example:
```yaml
- type: weather
options:
apiKey: VUE_APP_WEATHER_TOKEN
city: London
units: metric
hideDetails: true
```
Then, set `VUE_APP_WEATHER_TOKEN='xxx'`
---
### Setting Timeout
If the endpoint you are requesting data from is slow to respond, you may see a timeout error in the console. This can easily be fixed by specifying the `timeout` property on the offending widget. This should be an integer value, in milliseconds. By default timeout is `2500` ms (2½ seconds).

View File

@ -1,18 +1,17 @@
{
"name": "dashy",
"version": "2.1.2",
"version": "3.0.0",
"license": "MIT",
"main": "server",
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
"scripts": {
"start": "node server",
"dev": "vue-cli-service serve",
"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build",
"lint": "vue-cli-service lint",
"pm2-start": "npx pm2 start server.js",
"build-watch": "vue-cli-service build --watch --mode production",
"watch-config": "node services/watch-for-changes",
"build-and-start": "NODE_OPTIONS=--openssl-legacy-provider npm-run-all --parallel build-watch start",
"build-and-start": "NODE_OPTIONS=--openssl-legacy-provider npm-run-all --parallel build start",
"validate-config": "node services/config-validator",
"health-check": "node services/healthcheck",
"dependency-audit": "npx improved-yarn-audit --ignore-dev-deps"
@ -53,15 +52,18 @@
"@vue/cli-plugin-babel": "^4.5.15",
"@vue/cli-plugin-eslint": "^4.5.15",
"@vue/cli-plugin-pwa": "^4.5.15",
"@vue/cli-service": "^4.5.15",
"@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-service": "^4.5.19",
"@vue/eslint-config-standard": "^4.0.0",
"babel-eslint": "^10.0.1",
"copy-webpack-plugin": "6.4.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-plugin-vue": "^7.9.0",
"npm-run-all": "^4.1.5",
"sass": "^1.38.0",
"sass-loader": "^7.1.0",
"typescript": "^5.4.4",
"vue-cli-plugin-yaml": "^1.0.2",
"vue-svg-loader": "^0.16.0",
"vue-template-compiler": "^2.7.0"

View File

@ -18,7 +18,9 @@ const history = require('connect-history-api-fallback');
/* Kick of some basic checks */
require('./services/update-checker'); // Checks if there are any updates available, prints message
require('./services/config-validator'); // Include and kicks off the config file validation script
let config = {}; // setup the config
config = require('./services/config-validator'); // Include and kicks off the config file validation script
/* Include route handlers for API endpoints */
const statusCheck = require('./services/status-check'); // Used by the status check feature, uses GET
@ -27,6 +29,7 @@ const rebuild = require('./services/rebuild-app'); // A script to programmatical
const systemInfo = require('./services/system-info'); // Basic system info, for resource widget
const sslServer = require('./services/ssl-server'); // TLS-enabled web server
const corsProxy = require('./services/cors-proxy'); // Enables API requests to CORS-blocked services
const getUser = require('./services/get-user'); // Enables server side user lookup
/* Helper functions, and default config */
const printMessage = require('./services/print-message'); // Function to print welcome msg on start
@ -35,12 +38,15 @@ const ENDPOINTS = require('./src/utils/defaults').serviceEndpoints; // API endpo
/* 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);
/* Checks env var for port. If undefined, will use Port 8080 for Docker, or 4000 for metal */
const port = process.env.PORT || (isDocker ? 8080 : 4000);
/* Checks env var for host. If undefined, will use 0.0.0.0 */
const host = process.env.HOST || '0.0.0.0';
/* Indicates for the webpack config, that running as a server */
process.env.IS_SERVER = 'True';
/* Attempts to get the users local IP, used as part of welcome message */
const getLocalIp = () => {
const dnsLookup = util.promisify(dns.lookup);
@ -71,12 +77,8 @@ const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, n
const app = express()
// Load SSL redirection middleware
.use(sslServer.middleware)
// Serves up static files
.use(express.static(path.join(__dirname, 'dist')))
.use(express.static(path.join(__dirname, 'public'), { index: 'initialization.html' }))
// Load middlewares for parsing JSON, and supporting HTML5 history routing
.use(express.json({ limit: '1mb' }))
.use(history())
// GET endpoint to run status of a given URL with GET request
.use(ENDPOINTS.statusCheck, (req, res) => {
try {
@ -87,10 +89,11 @@ const app = express()
printWarning(`Error running status check for ${req.url}\n`, e);
}
})
// POST Endpoint used to save config, by writing conf.yml to disk
// POST Endpoint used to save config, by writing config file to disk
.use(ENDPOINTS.save, method('POST', (req, res) => {
try {
saveConfig(req.body, (results) => { res.end(results); });
config = req.body.config; // update the config
} catch (e) {
printWarning('Error writing config file to disk', e);
res.end(JSON.stringify({ success: false, message: e }));
@ -122,6 +125,20 @@ const app = express()
res.end(JSON.stringify({ success: false, message: e }));
}
})
// GET endpoint to return user info
.use(ENDPOINTS.getUser, (req, res) => {
try {
const user = getUser(config, req);
res.end(JSON.stringify(user));
} catch (e) {
res.end(JSON.stringify({ success: false, message: e }));
}
})
// Serves up static files
.use(express.static(path.join(__dirname, process.env.USER_DATA_DIR || 'user-data')))
.use(express.static(path.join(__dirname, 'dist')))
.use(express.static(path.join(__dirname, 'public'), { index: 'initialization.html' }))
.use(history())
// If no other route is matched, serve up the index.html with a 404 status
.use((req, res) => {
res.status(404).sendFile(path.join(__dirname, 'dist', 'index.html'));

View File

@ -98,11 +98,14 @@ const printFileReadError = (e) => {
}
};
let config = {};
try { // Try to open and parse the YAML file
const config = yaml.load(fs.readFileSync('./public/conf.yml', 'utf8'));
config = yaml.load(fs.readFileSync(`./${process.env.USER_DATA_DIR || 'user-data'}/conf.yml`, 'utf8'));
validate(config);
} catch (e) { // Something went very wrong...
setIsValidVariable(false);
logToConsole(bigError());
printFileReadError(e);
}
module.exports = config;

15
services/get-user.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = (config, req) => {
try {
if ( config.appConfig.auth.enableHeaderAuth ) {
const userHeader = config.appConfig.auth.headerAuth.userHeader;
const proxyWhitelist = config.appConfig.auth.headerAuth.proxyWhitelist;
if ( proxyWhitelist.includes(req.socket.remoteAddress) ) {
return { "success": true, "user": req.headers[userHeader.toLowerCase()] };
}
}
return {};
} catch (e) {
console.warn("Error get-user: ", e);
return { 'success': false };
}
};

View File

@ -6,11 +6,17 @@
const isSsl = !!process.env.SSL_PRIV_KEY_PATH && !!process.env.SSL_PUB_KEY_PATH;
// eslint-disable-next-line import/no-dynamic-require
const http = require(isSsl ? 'https' : 'http');
/* Location of the server to test */
const isDocker = !!process.env.IS_DOCKER;
const port = isSsl ? (process.env.SSL_PORT || (isDocker ? 443 : 4001)) : (process.env.PORT || (isDocker ? 80 : 4000));
/* Get the port to use (depending on, if docker, if SSL) */
const sslPort = process.env.SSL_PORT || (isDocker ? 443 : 4001);
const normalPort = process.env.PORT || (isDocker ? 8080 : 4000);
const port = isSsl ? sslPort : normalPort;
const host = process.env.HOST || '0.0.0.0';
const timeout = 2000;
@ -18,7 +24,9 @@ const agent = new http.Agent({
rejectUnauthorized: false, // Allow self-signed certificates
});
const requestOptions = { host, port, timeout, agent };
const requestOptions = {
host, port, timeout, agent,
};
const startTime = new Date(); // Initialize timestamp to calculate time taken

View File

@ -32,7 +32,7 @@ module.exports = (ip, port, isDocker) => {
} 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}Welcome to Dashy! 🚀${blanks(54)}${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}`
+ `${line(75)}${chars.BR}${chars.BR}${chars.RESET}`;

View File

@ -18,15 +18,15 @@ module.exports = async (newConfig, render) => {
// Define constants for the config file
const settings = {
defaultLocation: './public/',
defaultLocation: process.env.USER_DATA_DIR || './user-data/',
defaultFile: 'conf.yml',
filename: 'conf',
backupDenominator: '.backup.yml',
};
// Make the full file name and path to save the backup config file
const backupFilePath = path.normalize(process.env.BACKUP_DIR || settings.defaultLocation)
+ `/${usersFileName || settings.filename}-`
const backupFilePath = `${path.normalize(process.env.BACKUP_DIR || settings.defaultLocation)
}/${usersFileName || settings.filename}-`
+ `${Math.round(new Date() / 1000)}${settings.backupDenominator}`;
// The path where the main conf.yml should be read and saved to
@ -48,12 +48,12 @@ module.exports = async (newConfig, render) => {
// Makes a backup of the existing config file
await fsPromises
.copyFile(defaultFilePath, backupFilePath)
.catch((error) => render(getRenderMessage(false, `Unable to backup conf.yml: ${error}`)));
.catch((error) => render(getRenderMessage(false, `Unable to backup ${settings.defaultFile}: ${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 to conf.yml: ${error}`)));
.catch((error) => render(getRenderMessage(false, `Unable to write to ${settings.defaultFile}: ${error}`)));
// If successful, then render hasn't yet been called- call it
await render(getRenderMessage(true));

View File

@ -1,78 +0,0 @@
const fs = require('fs');
const { exec } = require('child_process');
const path = require('path');
const crypto = require('crypto');
// Default location of config file in container
const configFileName = '../public/conf.yml';
// Real path of config file in container
const configFilePath = path.resolve(__dirname, configFileName);
// Amount of time to ignore file after change detected
const debounceTimeMs = 2000;
// Store current timeout
let timeout = null;
// Store last hash of file
let lastHash = null;
/**
* Calculate hash of file, used for de-bounce mechanism to
* prevent successive updates if file content not changed
*/
const hashFileContent = (filePath) => {
const content = fs.readFileSync(filePath, 'utf8');
return crypto.createHash('sha256').update(content).digest('hex');
};
/**
* Just logs a given message to terminal so user knows what's happening
*/
const logInfo = (message, msgLevel = 'OUTPUT') => {
const RESET = '\x1b[0m';
let logLevels = {};
switch (msgLevel) {
case 'ERROR': logLevels = { col: '\x1b[31m', func: console.error }; break;
case 'WARNING': logLevels = { col: '\x1b[33m', func: console.warn }; break;
case 'INFO': logLevels = { col: '\x1b[36m', func: console.info }; break;
case 'SUCCESS': logLevels = { col: '\x1b[32m', func: console.log }; break;
default: logLevels = { col: RESET, func: console.log };
}
logLevels.func(`${logLevels.col}\x1b[1m[${msgLevel}]${RESET} ${logLevels.col}${message}${RESET}\n`);
};
// Log initial message to user
logInfo(`When '${configFileName}' is updated, a rebuild will be triggered.\n`);
/**
* Code to be executed when a watch event is triggered
* Will check correctly expected file and time frame,
* then ensure the hash is different from last hash,
* and then trigger -rebuild of frontend with yarn build
* outputting the stdrout and stderr to user's terminal
*/
const watchAction = (eventType, filename) => {
if (filename && eventType === 'change') {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
const currentHash = hashFileContent(configFilePath);
if (currentHash !== lastHash) {
lastHash = currentHash;
logInfo(`${filename} file Changed, running build...`);
exec('yarn build', (error, stdout, stderr) => {
if (error) {
logInfo(error, 'ERROR');
return;
}
logInfo(stdout);
logInfo(stderr, 'WARNING');
logInfo('Build completed successfully.\n', 'SUCCESS');
});
} else {
logInfo(`${filename} file Detected change, but content is the same. Skipping....`, 'WARNING');
}
}, debounceTimeMs);
}
};
// Watch given config path, with the watch action function
fs.watch(configFilePath, watchAction);

View File

@ -64,7 +64,7 @@ export default {
return this.$store.getters.pageInfo;
},
sections() {
return this.$store.getters.pageInfo;
return this.$store.getters.sections;
},
visibleComponents() {
return this.$store.getters.visibleComponents;

View File

@ -171,9 +171,9 @@
"status-fail-msg": "Task Failed",
"success-msg-disk": "Config file written to disk successfully",
"success-msg-local": "Local changes saved successfully",
"success-note-l1": "The app should rebuild automatically.",
"success-note-l2": "This may take up to a minute.",
"success-note-l3": "You will need to refresh the page for changes to take effect.",
"success-note-l1": "You will need to refresh the page for changes to take effect.",
"success-note-l2": "",
"success-note-l3": "",
"error-msg-save-mode": "Please select a Save Mode: Local or File",
"error-msg-cannot-save": "An error occurred saving config",
"error-msg-bad-json": "Error in JSON, possibly malformed",
@ -182,9 +182,9 @@
},
"app-rebuild": {
"title": "Rebuild Application",
"rebuild-note-l1": "A rebuild is required for changes written to the conf.yml file to take effect.",
"rebuild-note-l2": "This should happen automatically, but if it hasn't, you can manually trigger it here.",
"rebuild-note-l3": "This is not required for modifications stored locally.",
"rebuild-note-l1": "A rebuild is no longer required for changes to take effect.",
"rebuild-note-l2": "Some changes (entry-point, and auth settings) are read at build-time. So to apply these, you should trigger a rebuild here.",
"rebuild-note-l3": "Note that this is only available on Node and Docker installations, not via statically deployed instances.",
"rebuild-button": "Start Build",
"rebuilding-status-1": "Building...",
"rebuilding-status-2": "This may take a few minutes",

View File

@ -155,12 +155,23 @@ export default {
},
/* When restored data is revieved, then save to local storage, and apply it in state */
applyRestoredData(config, backupId) {
// Store restored data in local storage
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
if (config.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
const isSubPage = !!this.$store.state.currentConfigInfo.confId;
if (isSubPage) { // Apply to sub-page only
const subConfigId = this.$store.state.currentConfigInfo.confId;
const sectionStorageKey = `${localStorageKeys.CONF_SECTIONS}-${subConfigId}`;
const pageInfoStorageKey = `${localStorageKeys.PAGE_INFO}-${subConfigId}`;
const themeStoreKey = `${localStorageKeys.THEME}-${subConfigId}`;
localStorage.setItem(sectionStorageKey, JSON.stringify(config.sections));
localStorage.setItem(pageInfoStorageKey, JSON.stringify(config.pageInfo));
localStorage.setItem(themeStoreKey, config.appConfig.theme);
} else { // Apply to main config
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
localStorage.setItem(localStorageKeys.CONF_PAGES, JSON.stringify(config.pages || []));
if (config.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
}
}
// Save hashed token in local storage
this.setBackupIdLocally(backupId, this.restorePassword);

View File

@ -47,16 +47,17 @@
</Button>
<!-- Display app version and language -->
<p class="language">{{ getLanguage() }}</p>
<p v-if="$store.state.currentConfigInfo" class="config-location">
Using Config From<br>
{{ $store.state.currentConfigInfo.confPath }}
<!-- Display location of config file -->
<p class="config-location">
Using config from
<a :href="configPath">{{ configPath }}</a>
</p>
<AppVersion />
</div>
<!-- Display note if Config disabled, or if on mobile -->
<p v-if="!enableConfig" class="config-disabled-note">{{ $t('config.disabled-note') }}</p>
<p class="small-screen-note" style="display: none;">{{ $t('config.small-screen-note') }}</p>
<div class="config-note">
<div class="config-note" @click="openExportConfigModal">
<span>{{ $t('config.backup-note') }}</span>
</div>
</div>
@ -116,6 +117,11 @@ export default {
enableConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
configPath() {
return this.$store.state.currentConfigInfo?.confPath
|| process.env.VUE_APP_CONFIG_PATH
|| '/conf.yml';
},
},
components: {
Button,
@ -248,8 +254,12 @@ a.hyperlink-wrapper {
p.app-version, p.language, p.config-location {
margin: 0.5rem auto;
font-size: 1rem;
color: var(--transparent-white-50);
color: var(--config-settings-color);
cursor: default;
opacity: var(--dimming-factor);
a {
color: var(--config-settings-color);
}
}
div.code-container {

View File

@ -143,7 +143,11 @@ export default {
this.$modal.hide(modalNames.CONF_EDITOR);
},
writeToDisk() {
this.writeConfigToDisk(this.config);
const newData = this.jsonData;
this.writeConfigToDisk(newData);
// this.$store.commit(StoreKeys.SET_APP_CONFIG, newData.appConfig);
this.$store.commit(StoreKeys.SET_PAGE_INFO, newData.pageInfo);
this.$store.commit(StoreKeys.SET_SECTIONS, newData.sections);
},
saveLocally() {
const msg = this.$t('interactive-editor.menu.save-locally-warning');

View File

@ -22,6 +22,14 @@
<DownloadConfigIcon />
</Button>
</div>
<!-- Show path to which config file is being used -->
<div class="config-path-info">
<h3>Config Location</h3>
<p>
The base config file you are currently using is
<a :href="configPath">{{ configPath }}</a>
</p>
</div>
<!-- View Config in Tree Mode Section -->
<h3>{{ $t('interactive-editor.export.view-title') }}</h3>
<tree-view :data="config" class="config-tree-view" />
@ -61,6 +69,11 @@ export default {
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
configPath() {
return this.$store.state.currentConfigInfo?.confPath
|| process.env.VUE_APP_CONFIG_PATH
|| '/conf.yml';
},
},
methods: {
convertJsonToYaml() {
@ -121,6 +134,13 @@ export default {
border-bottom: 1px dashed var(--interactive-editor-color);
button { margin: 0 1rem; }
}
.config-path-info {
p, a {
color: var(--interactive-editor-color);
font-size: 1.2rem;
}
border-bottom: 1px dashed var(--interactive-editor-color);
}
.config-tree-view {
padding: 0.5rem;
font-family: var(--font-monospace);

View File

@ -1,16 +1,13 @@
<template>
<!-- User Footer -->
<footer v-if="text && text !== '' && visible" v-html="text"></footer>
<!-- Default Footer -->
<footer v-else-if="visible">
<span v-if="$store.state.currentConfigInfo" class="path-to-config">
Using: {{ $store.state.currentConfigInfo.confPath }}
</span>
<span>
{{ $t('footer.dev-by') }} <a :href="authorUrl">{{authorName}}</a>.
{{ $t('footer.licensed-under') }} <a :href="licenseUrl">{{license}}</a>
{{ showCopyright? '©': '' }} {{date}}.
{{ $t('footer.get-the') }} <a :href="repoUrl">{{ $t('footer.source-code') }}</a>.
<footer v-if="visible">
<!-- User-defined footer -->
<span v-if="text" v-html="text"></span>
<!-- Default footer -->
<span v-else>
<a :href="defaultInfo.projectUrl">Dashy</a> is free & open source
- licensed under <a :href="defaultInfo.licenseUrl">{{defaultInfo.license}}</a>,
© <a :href="defaultInfo.authorUrl">{{defaultInfo.authorName}}</a> {{defaultInfo.date}}.
Get support on GitHub, at <a :href="defaultInfo.repoUrl">{{defaultInfo.repoName}}</a>.
</span>
</footer>
</template>
@ -23,13 +20,20 @@ export default {
name: 'Footer',
props: {
text: String,
authorName: { type: String, default: 'Alicia Sykes' },
authorUrl: { type: String, default: 'https://aliciasykes.com' },
license: { type: String, default: 'MIT' },
licenseUrl: { type: String, default: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17' },
date: { type: String, default: `${new Date().getFullYear()}` },
showCopyright: { type: Boolean, default: true },
repoUrl: { type: String, default: 'https://github.com/lissy93/dashy' },
},
data() {
return {
defaultInfo: {
authorName: 'Alicia Sykes',
authorUrl: 'https://as93.net',
license: 'MIT',
licenseUrl: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17',
date: `${new Date().getFullYear()}`,
repoUrl: 'https://github.com/lissy93/dashy',
repoName: 'Lissy93/Dashy',
projectUrl: 'https://dashy.to',
},
};
},
computed: {
visible() {
@ -56,7 +60,7 @@ footer {
display: none;
}
span.path-to-config {
float: right;
float: left;
font-size: 0.75rem;
margin: 0.1rem 0.5rem 0 0;
opacity: var(--dimming-factor);

View File

@ -66,7 +66,7 @@ export default {
span.subtitle {
color: var(--heading-text-color);
font-style: italic;
text-shadow: 1px 1px 2px #130f23;
text-shadow: 1px 1px 2px #130f2347;
opacity: var(--dimming-factor);
}
img.site-logo {

View File

@ -1,36 +1,70 @@
<template>
<transition name="slide-fade">
<div class="kb-sc-info" v-if="!shouldHide">
<h5>There are keyboard shortcuts! 🙌</h5>
<h5>{{ popupContent.title }}</h5>
<div class="close" title="Hide forever [Esc]" @click="hideWelcomeHelper()">x</div>
<p title="Press [Esc] to hide this tip forever. See there's even a shortcut for that! 🚀">
Just start typing to filter. Then use the tab key to cycle through results,
and press enter to launch the selected item, or alt + enter to open in a modal.
You can hit Esc at anytime to clear the search. Easy 🥳
</p>
<p :title="popupContent.hoverText">{{ popupContent.message }}</p>
<p :title="popupContent.hoverText">{{ popupContent.messageContinued }}</p>
<div class="action-buttons">
<button @click="exportConfig">Export Local Config</button>
<button @click="saveConfig">Save Changes to Disk</button>
<button @click="resetLocalConfig">Reset Local Changes</button>
<button @click="hideWelcomeHelper">Dismiss this Notification</button>
</div>
</div>
</transition>
</template>
<script>
import { localStorageKeys } from '@/utils/defaults';
import { localStorageKeys, modalNames } from '@/utils/defaults';
import StoreKeys from '@/utils/StoreMutations';
import configSavingMixin from '@/mixins/ConfigSaving';
export default {
name: 'KeyboardShortcutInfo',
mixins: [configSavingMixin],
data() {
return {
shouldHide: true, // False = show/ true = hide. Intuitive, eh?
timeDelay: 3000, // Short delay in ms before popup appears
timeDelay: 2000, // Short delay in ms before popup appears
popupContent: {
title: '⚠️ You\'re using a local config',
message: `This means that your settings are saved in this browser only,
and won't persist across devices.`,
messageContinued: `To ensure you don't loose your changes,
it's recommended to download a copy of your config, so you can restore it later.`,
hoverText: 'Press [Esc] to hide this warning',
},
};
},
methods: {
exportConfig() {
this.$modal.show(modalNames.EXPORT_CONFIG_MENU);
this.shouldHide = true;
},
saveConfig() {
const localConfig = this.$store.state.config;
this.writeConfigToDisk(localConfig);
this.shouldHide = true;
},
resetLocalConfig() {
const msg = `${this.$t('config.reset-config-msg-l1')} `
+ `${this.$t('config.reset-config-msg-l2')}\n\n${this.$t('config.reset-config-msg-l3')}`;
const isTheUserSure = confirm(msg); // eslint-disable-line no-alert, no-restricted-globals
if (isTheUserSure) {
localStorage.clear();
this.$toasted.show(this.$t('config.data-cleared-msg'));
this.$store.dispatch(StoreKeys.INITIALIZE_CONFIG);
this.shouldHide = true;
}
},
/**
* Returns true if the key exists in session storage, otherwise false
* And the !! just converts 'false' to false, as strings resolve to true
*/
shouldHideWelcomeMessage() {
return !!localStorage[localStorageKeys.HIDE_WELCOME_BANNER];
return !!localStorage[localStorageKeys.HIDE_INFO_NOTIFICATION];
},
/**
* Update session storage, so that it won't be shown again
@ -38,7 +72,7 @@ export default {
*/
hideWelcomeHelper() {
this.shouldHide = true;
localStorage.setItem(localStorageKeys.HIDE_WELCOME_BANNER, true);
localStorage.setItem(localStorageKeys.HIDE_INFO_NOTIFICATION, true);
window.removeEventListener('keyup', this.keyPressEvent);
},
/* Passed to window function, to add/ remove event listener */
@ -114,6 +148,23 @@ export default {
}
}
}
.action-buttons {
display: flex;
justify-content: space-around;
margin-top: 1em;
button {
padding: 0.2rem;
background: var(--welcome-popup-background);
color: var(--welcome-popup-text-color);
border: 1px solid var(--welcome-popup-text-color);
border-radius: var(--curve-factor);
transition: all 0.2s ease-in-out;
&:hover {
background: var(--welcome-popup-text-color);
color: var(--welcome-popup-background);
}
}
}
/* Animations, animations everywhere */
.slide-fade-enter-active {
transition: all 1s ease;

View File

@ -95,7 +95,8 @@ export default {
},
/* If configured, launch specific app when hotkey pressed */
handleHotKey(key) {
const usersHotKeys = this.getCustomKeyShortcuts();
const sections = this.$store.getters.sections || [];
const usersHotKeys = this.getCustomKeyShortcuts(sections);
usersHotKeys.forEach((hotkey) => {
if (hotkey.hotkey === parseInt(key, 10)) {
if (hotkey.url) window.open(hotkey.url, '_blank');

View File

@ -8,7 +8,7 @@
:value="$store.getters.theme"
class="theme-dropdown"
:tabindex="-2"
@input="themeChanged"
@input="themeChangedInUI"
/>
</div>
<IconPalette
@ -28,18 +28,13 @@
<script>
import CustomThemeMaker from '@/components/Settings/CustomThemeMaker';
import {
LoadExternalTheme,
ApplyLocalTheme,
ApplyCustomVariables,
} from '@/utils/ThemeHelper';
import Defaults, { localStorageKeys } from '@/utils/defaults';
import Keys from '@/utils/StoreMutations';
import ErrorHandler from '@/utils/ErrorHandler';
import IconPalette from '@/assets/interface-icons/config-color-palette.svg';
import ThemingMixin from '@/mixins/ThemingMixin';
export default {
name: 'ThemeSelector',
mixins: [ThemingMixin],
props: {
hidePallete: Boolean,
},
@ -47,101 +42,16 @@ export default {
CustomThemeMaker,
IconPalette,
},
watch: {
/* When theme in VueX store changes, then update theme */
themeFromStore(newTheme) {
this.selectedTheme = newTheme;
this.updateTheme(newTheme);
},
},
data() {
return {
selectedTheme: '',
themeConfiguratorOpen: false, // Control the opening of theme config popup
themeHelper: new LoadExternalTheme(),
ApplyLocalTheme,
ApplyCustomVariables,
};
},
computed: {
/* Get appConfig from store */
appConfig() {
return this.$store.getters.appConfig;
},
/* Get users theme from store */
themeFromStore() {
return this.$store.getters.theme;
},
/* Combines all theme names (builtin and user defined) together */
themeNames: function themeNames() {
const externalThemeNames = Object.keys(this.externalThemes);
const specialThemes = ['custom'];
return [...this.extraThemeNames, ...externalThemeNames,
...Defaults.builtInThemes, ...specialThemes];
},
extraThemeNames() {
const userThemes = this.appConfig.cssThemes || [];
if (typeof userThemes === 'string') return [userThemes];
return userThemes;
},
/* Returns an array of links to external CSS from the Config */
externalThemes() {
const availibleThemes = {};
if (this.appConfig && this.appConfig.externalStyleSheet) {
const externals = this.appConfig.externalStyleSheet;
if (Array.isArray(externals)) {
externals.forEach((ext, i) => {
availibleThemes[`External Stylesheet ${i + 1}`] = ext;
});
} else if (typeof externals === 'string') {
availibleThemes['External Stylesheet'] = this.appConfig.externalStyleSheet;
} else {
ErrorHandler('External stylesheets must be of type string or string[]');
}
}
// availibleThemes.Default = '#';
return availibleThemes;
},
},
computed: {},
mounted() {
const initialTheme = this.getInitialTheme();
this.selectedTheme = initialTheme;
// Quicker loading, if the theme is local we can apply it immidiatley
if (this.isThemeLocal(initialTheme)) {
this.updateTheme(initialTheme);
}
// If it's an external stylesheet, then wait for promise to resolve
if (this.externalThemes && Object.entries(this.externalThemes).length > 0) {
const added = Object.keys(this.externalThemes).map(
name => this.themeHelper.add(name, this.externalThemes[name]),
);
// Once, added, then apply users initial theme
Promise.all(added).then(() => {
this.updateTheme(initialTheme);
});
}
this.initializeTheme();
},
methods: {
/* Called when dropdown changed
* Updates store, which will in turn update theme through watcher
*/
themeChanged() {
const pageId = this.$store.state.currentConfigInfo?.pageId || null;
this.$store.commit(Keys.SET_THEME, { theme: this.selectedTheme, pageId });
this.updateTheme(this.selectedTheme);
},
/* Returns the initial theme */
getInitialTheme() {
const localTheme = localStorage[localStorageKeys.THEME];
if (localTheme && localTheme !== 'undefined') return localTheme;
return this.appConfig.theme || Defaults.theme;
},
/* Determines if a given theme is local / not a custom user stylesheet */
isThemeLocal(themeToCheck) {
const localThemes = [...Defaults.builtInThemes, ...this.extraThemeNames];
return localThemes.includes(themeToCheck);
},
/* Opens the theme color configurator popup */
openThemeConfigurator() {
this.$store.commit(Keys.SET_MODAL_OPEN, true);
@ -154,24 +64,6 @@ export default {
this.themeConfiguratorOpen = false;
}
},
/* Updates theme. Checks if the new theme is local or external,
and calls appropirate updating function. Updates local storage */
updateTheme(newTheme) {
if (newTheme === 'Default') {
this.resetToDefault();
this.themeHelper.theme = 'Default';
} else if (this.isThemeLocal(newTheme)) {
this.ApplyLocalTheme(newTheme);
} else {
this.themeHelper.theme = newTheme;
}
this.ApplyCustomVariables(newTheme);
// localStorage.setItem(localStorageKeys.THEME, newTheme);
},
/* Removes any applied themes */
resetToDefault() {
document.getElementsByTagName('html')[0].removeAttribute('data-theme');
},
},
};
</script>

View File

@ -26,7 +26,7 @@ export default {
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
hostname() {
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
showFullInfo() {
return this.options.showFullInfo;
@ -39,7 +39,9 @@ export default {
},
authHeaders() {
if (this.options.username && this.options.password) {
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
const password = this.parseAsEnvVar(this.options.password);
const username = this.parseAsEnvVar(this.options.username);
const encoded = window.btoa(`${username}:${password}`);
return { Authorization: `Basic ${encoded}` };
}
return {};

View File

@ -38,7 +38,7 @@ export default {
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
hostname() {
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
showOnOffStatusOnly() {
return this.options.showOnOffStatusOnly;
@ -48,7 +48,9 @@ export default {
},
authHeaders() {
if (this.options.username && this.options.password) {
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
const username = this.parseAsEnvVar(this.options.username);
const password = this.parseAsEnvVar(this.options.password);
const encoded = window.btoa(`${username}:${password}`);
return { Authorization: `Basic ${encoded}` };
}
return {};

View File

@ -20,14 +20,16 @@ export default {
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
hostname() {
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
endpoint() {
return `${this.hostname}/control/stats`;
},
authHeaders() {
if (this.options.username && this.options.password) {
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
const username = this.parseAsEnvVar(this.options.username);
const password = this.parseAsEnvVar(this.options.password);
const encoded = window.btoa(`${username}:${password}`);
return { Authorization: `Basic ${encoded}` };
}
return {};

View File

@ -36,11 +36,13 @@ export default {
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
hostname() {
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
authHeaders() {
if (this.options.username && this.options.password) {
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
const username = this.parseAsEnvVar(this.options.username);
const password = this.parseAsEnvVar(this.options.password);
const encoded = window.btoa(`${username}:${password}`);
return { Authorization: `Basic ${encoded}` };
}
return {};

View File

@ -113,7 +113,7 @@ export default {
},
computed: {
hostname() {
return this.options.hostname || widgetApiEndpoints.anonAddy;
return this.parseAsEnvVar(this.options.hostname) || widgetApiEndpoints.anonAddy;
},
apiVersion() {
return this.options.apiVersion || 'v1';
@ -132,7 +132,7 @@ export default {
},
apiKey() {
if (!this.options.apiKey) this.error('An apiKey is required');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
hideMeta() {
return this.options.hideMeta;

View File

@ -35,7 +35,7 @@ export default {
},
apiKey() {
if (!this.options.apiKey) this.error('Missing API Key');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
endpoint() {
return `${widgetApiEndpoints.blacklistCheck}/${this.ipAddress}`;

View File

@ -38,12 +38,12 @@ export default {
/* The username to fetch data from - REQUIRED */
username() {
if (!this.options.username) this.error('You must specify a username');
return this.options.username;
return this.parseAsEnvVar(this.options.username);
},
/* Optionally override hostname, if using a self-hosted instance */
hostname() {
if (this.options.hostname) return this.options.hostname;
return widgetApiEndpoints.codeStats;
return this.parseAsEnvVar(widgetApiEndpoints.codeStats);
},
hideMeta() {
return this.options.hideMeta || false;

View File

@ -63,11 +63,11 @@ export default {
computed: {
apiKey() {
if (!this.options.apiKey) this.error('Missing API Key');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
domain() {
if (!this.options.domain) this.error('Missing Domain Name Key');
return this.options.domain;
return this.parseAsEnvVar(this.options.domain);
},
endpoint() {
return `${widgetApiEndpoints.domainMonitor}/?domain=${this.domain}&r=whois&apikey=${this.apiKey}`;

View File

@ -106,7 +106,7 @@ export default {
if (!this.options.apiKey) {
this.error('An API key is required, please see the docs for more info');
}
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
},
methods: {

View File

@ -45,7 +45,7 @@ export default {
computed: {
/* The users API key for exchangerate-api.com */
apiKey() {
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
/* The currency to convert results into */
inputCurrency() {

View File

@ -71,7 +71,7 @@ export default {
this.error('An API key must be supplied');
return '';
}
return usersChoice;
return this.parseAsEnvVar(usersChoice);
},
/* The direction of flights: Arrival, Departure or Both */
direction() {

View File

@ -58,7 +58,7 @@ export default {
},
hostname() {
if (!this.options.hostname) this.error('`hostname` is required');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
},
methods: {

View File

@ -56,7 +56,7 @@ export default {
this.error('An API key is required, please see the docs for more info');
}
if (typeof this.options.apiKey === 'string') {
return [this.options.apiKey];
return [this.parseAsEnvVar(this.options.apiKey)];
}
return this.options.apiKey;
},

View File

@ -30,11 +30,11 @@ export default {
computed: {
endpoint() {
if (!this.options.host) this.error('linkgding Host is required');
return `${this.options.host}/api/bookmarks`;
return `${this.parseAsEnvVar(this.options.host)}/api/bookmarks`;
},
apiKey() {
if (!this.options.apiKey) this.error('linkgding apiKey is required');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
filtertags() {
return this.options.tags;

View File

@ -29,7 +29,7 @@ export default {
computed: {
apiKey() {
if (!this.options.apiKey) this.error('An API key is required, see docs for more info');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
country() {
return this.options.country ? `&country=${this.options.country}` : '';

View File

@ -22,7 +22,7 @@
</span>
<span v-if="canDeleteNotification('delete')">
<a @click="deleteNotification(notification.notification_id)"
class="action secondary">{{ tt('delete-notification') }}</a>
class="action secondary">{{ tt('delete-notification') }}</a>
</span>
</p>
</div>

View File

@ -44,7 +44,7 @@
<em v-html="formatNumber(shares.num_shares)"></em>
<strong>{{ tt('local') }}</strong> <small> {{ tt('and') }}</small>
<em v-html="formatNumber(shares.num_fed_shares_sent
+ shares.num_fed_shares_received)"></em>
+ shares.num_fed_shares_received)"></em>
<strong>
{{ tt('federated-shares') }}
</strong>

View File

@ -36,13 +36,14 @@ export default {
computed: {
/* Let user select which comic to display: random, latest or a specific number */
hostname() {
const usersChoice = this.options.hostname;
const usersChoice = this.parseAsEnvVar(this.options.hostname);
if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server');
return usersChoice || 'http://pi.hole';
},
apiKey() {
if (!this.options.apiKey) this.error('API Key is required, please see the docs');
return this.options.apiKey;
const usersChoice = this.parseAsEnvVar(this.options.apiKey);
if (!usersChoice) this.error('API Key is required, please see the docs');
return usersChoice;
},
endpoint() {
return `${this.hostname}/admin/api.php?summary&auth=${this.apiKey}`;

View File

@ -34,22 +34,22 @@ export default {
computed: {
clusterUrl() {
if (!this.options.cluster_url) this.error('The cluster URL is required.');
return this.options.cluster_url || '';
return this.parseAsEnvVar(this.options.cluster_url) || '';
},
userName() {
if (!this.options.user_name) this.error('The user name is required.');
return this.options.user_name || '';
return this.parseAsEnvVar(this.options.user_name) || '';
},
tokenName() {
if (!this.options.token_name) this.error('The token name is required.');
return this.options.token_name || '';
return this.parseAsEnvVar(this.options.token_name) || '';
},
tokenUuid() {
if (!this.options.token_uuid) this.error('The token uuid is required.');
return this.options.token_uuid || '';
return this.parseAsEnvVar(this.options.token_uuid) || '';
},
node() {
return this.options.node || '';
return this.parseAsEnvVar(this.options.node) || '';
},
nodeData() {
return this.options.node_data || false;

View File

@ -35,7 +35,7 @@ export default {
},
provider() {
// Can be either `ip-api`, `ipapi.co` or `ipgeolocation`
return this.options.provider || 'ipapi.co';
return this.parseAsEnvVar(this.options.provider) || 'ipapi.co';
},
},
data() {

View File

@ -51,7 +51,7 @@ export default {
return this.options.rssUrl || '';
},
apiKey() {
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
parseLocally() {
return this.options.parseLocally;

View File

@ -93,7 +93,7 @@ export default {
return this.options.leagueId;
},
apiKey() {
return this.options.apiKey || '50130162';
return this.parseAsEnvVar(this.options.apiKey) || '50130162';
},
limit() {
return this.options.limit || 20;

View File

@ -29,7 +29,7 @@ export default {
},
/* The users API key for AlphaVantage */
apiKey() {
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
/* The formatted GET request API endpoint to fetch stock data from */
endpoint() {

View File

@ -45,15 +45,15 @@ export default {
computed: {
hostname() {
if (!this.options.hostname) this.error('A hostname is required');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
username() {
if (!this.options.username) this.error('A username is required');
return this.options.username;
return this.parseAsEnvVar(this.options.username);
},
password() {
if (!this.options.password) this.error('A password is required');
return this.options.password;
return this.parseAsEnvVar(this.options.password);
},
endpointLogin() {
return `${this.hostname}/webapi/auth.cgi?api=SYNO.API.Auth&version=3&method=login&account=${this.username}&passwd=${this.password}&session=DownloadStation&format=sid`;

View File

@ -52,15 +52,11 @@ export default {
computed: {
/* Get API key for access to instance */
apiKey() {
const { apiKey } = this.options;
return apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
/* Get instance URL */
url() {
const { url } = this.options;
return url;
return this.parseAsEnvVar(this.options.url);
},
/* Create authorisation header for the instance from the apiKey */
authHeaders() {

View File

@ -53,7 +53,7 @@ export default {
},
address() {
if (!this.options.address) this.error('You must specify a public address');
return this.options.address;
return this.parseAsEnvVar(this.options.address);
},
network() {
return this.options.network || 'main';

View File

@ -46,13 +46,12 @@ export default {
return this.options.units || 'metric';
},
endpoint() {
const {
apiKey, city, lat, lon,
} = this.options;
if (lat && lon) {
return `${widgetApiEndpoints.weather}?lat=${lat}&lon=${lon}&appid=${apiKey}&units=${this.units}`;
}
return `${widgetApiEndpoints.weather}?q=${city}&appid=${apiKey}&units=${this.units}`;
const apiKey = this.parseAsEnvVar(this.options.apiKey);
const { city, lat, lon } = this.options;
const params = (lat && lon)
? `lat=${lat}&lon=${lon}&appid=${apiKey}&units=${this.units}`
: `q=${city}&appid=${apiKey}&units=${this.units}`;
return `${widgetApiEndpoints.weather}?${params}`;
},
tempDisplayUnits() {
switch (this.units) {

View File

@ -8,7 +8,6 @@
</template>
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import { widgetApiEndpoints } from '@/utils/defaults';
@ -41,11 +40,17 @@ export default {
methods: {
/* Make GET request to CoinGecko API endpoint */
fetchData() {
axios.get(this.endpoint)
.then((response) => {
this.processData(response.data);
fetch(this.endpoint)
.then(response => {
if (!response.ok) {
this.error('Network response was not ok');
}
return response.json();
})
.catch((dataFetchError) => {
.then(data => {
this.processData(data);
})
.catch(dataFetchError => {
this.error('Unable to fetch data', dataFetchError);
})
.finally(() => {
@ -71,7 +76,7 @@ export default {
<style scoped lang="scss">
.xkcd-wrapper {
.xkcd-title {
.xkcd-title {
font-size: 1.2rem;
margin: 0.25rem auto;
color: var(--widget-text-color);

View File

@ -13,14 +13,16 @@ import TreeView from 'vue-json-tree-view';
// Import base Dashy components and utils
import Dashy from '@/App.vue'; // Main Dashy Vue app
import router from '@/router'; // Router, for navigation
import store from '@/store'; // Store, for local state management
import router from '@/router'; // Router, for navigation
import serviceWorker from '@/utils/InitServiceWorker'; // Service worker initialization
import { messages } from '@/utils/languages'; // Language texts
import ErrorReporting from '@/utils/ErrorReporting'; // Error reporting initializer (off)
import clickOutside from '@/directives/ClickOutside'; // Directive for closing popups, modals, etc
import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/utils/defaults';
import { initKeycloakAuth, isKeycloakEnabled } from '@/utils/KeycloakAuth';
import { initHeaderAuth, isHeaderAuthEnabled } from '@/utils/HeaderAuth';
import Keys from '@/utils/StoreMutations';
// Initialize global Vue components
Vue.use(VueI18n);
@ -58,11 +60,17 @@ const mount = () => new Vue({
store, router, render, i18n,
}).$mount('#app');
// If Keycloak not enabled, then proceed straight to the app
if (!isKeycloakEnabled()) {
mount();
} else { // Keycloak is enabled, redirect to KC login page
initKeycloakAuth()
.then(() => mount())
.catch(() => window.location.reload());
}
store.dispatch(Keys.INITIALIZE_CONFIG).then(() => {
// Keycloak is enabled, redirect to KC login page
if (isKeycloakEnabled()) {
initKeycloakAuth()
.then(() => mount())
.catch(() => window.location.reload());
} else if (isHeaderAuthEnabled()) {
initHeaderAuth()
.then(() => mount())
.catch(() => window.location.reload());
} else { // If Keycloak not enabled, then proceed straight to the app
mount();
}
});

View File

@ -21,11 +21,17 @@ export default {
return;
}
// 1. Get the config, and strip appConfig if is sub-page
const isSubPag = !!this.$store.state.currentConfigInfo;
const isSubPag = !!this.$store.state.currentConfigInfo.confId;
const jsonConfig = config;
if (isSubPag) delete jsonConfig.appConfig;
jsonConfig.sections = jsonConfig.sections.map(({ filteredItems, ...section }) => section);
// If a sub-config, then remove appConfig, and check path isn't an external URL
if (isSubPag) {
delete jsonConfig.appConfig;
if (this.$store.state.currentConfigInfo.confPath.includes('http')) {
ErrorHandler('Cannot save to an external URL');
return;
}
}
// 2. Convert JSON into YAML
const yamlOptions = {};
const strjsonConfig = JSON.stringify(jsonConfig);
@ -67,20 +73,39 @@ export default {
ErrorHandler('Unable to save changes locally, this feature has been disabled');
return;
}
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
const isSubPag = !!this.$store.state.currentConfigInfo.confId;
if (isSubPag) { // Save for sub-page only
const configId = this.$store.state.currentConfigInfo.confId;
const localStorageKeySections = `${localStorageKeys.CONF_SECTIONS}-${configId}`;
const localStorageKeyPageInfo = `${localStorageKeys.PAGE_INFO}-${configId}`;
localStorage.setItem(localStorageKeySections, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeyPageInfo, JSON.stringify(config.pageInfo));
} else { // Or save to main config
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
}
if (config.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
}
InfoHandler('Config has succesfully been saved in browser storage', 'Config Update');
InfoHandler('Config has successfully been saved in browser storage', 'Config Update');
this.showToast(this.$t('config-editor.success-msg-local'), true);
this.$store.commit(StoreKeys.SET_EDIT_MODE, false);
},
carefullyClearLocalStorage() {
// Delete the main keys
localStorage.removeItem(localStorageKeys.PAGE_INFO);
localStorage.removeItem(localStorageKeys.APP_CONFIG);
localStorage.removeItem(localStorageKeys.CONF_SECTIONS);
// Then, if we've got any sub-pages, delete those too
(this.$store.getters.pages || []).forEach((page) => {
const localStorageKeySections = `${localStorageKeys.CONF_SECTIONS}-${page.id}`;
const localStorageKeyPageInfo = `${localStorageKeys.PAGE_INFO}-${page.id}`;
localStorage.removeItem(localStorageKeySections);
localStorage.removeItem(localStorageKeyPageInfo);
});
},
},
};

View File

@ -6,7 +6,6 @@ import Defaults, { localStorageKeys, iconCdns } from '@/utils/defaults';
import Keys from '@/utils/StoreMutations';
import { searchTiles } from '@/utils/Search';
import { checkItemVisibility } from '@/utils/CheckItemVisibility';
import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper';
const HomeMixin = {
props: {
@ -36,22 +35,28 @@ const HomeMixin = {
searchValue: '',
}),
async mounted() {
await this.getConfigForRoute();
// await this.getConfigForRoute();
},
watch: {
async $route() {
await this.getConfigForRoute();
this.setTheme();
this.loadUpConfig();
},
},
async created() {
this.loadUpConfig();
},
methods: {
async getConfigForRoute() {
this.$store.commit(Keys.SET_CURRENT_SUB_PAGE, this.subPageInfo);
if (this.subPageInfo && this.subPageInfo.confPath) { // Get config for sub-page
await this.$store.dispatch(Keys.INITIALIZE_MULTI_PAGE_CONFIG, this.subPageInfo.confPath);
} else { // Otherwise, use main config
this.$store.commit(Keys.USE_MAIN_CONFIG);
}
/* When page loaded / sub-page changed, initiate config fetch */
async loadUpConfig() {
const subPage = this.determineConfigFile();
await this.$store.dispatch(Keys.INITIALIZE_CONFIG, subPage);
},
/* Based on the current route, get which config to display, null will use default */
determineConfigFile() {
const pagePath = this.$router.currentRoute.path;
const isSubPage = new RegExp((/(home|workspace|minimal)\/[a-zA-Z0-9-]+/g)).test(pagePath);
const subPageName = isSubPage ? pagePath.split('/').pop() : null;
return subPageName;
},
/* TEMPORARY: If on sub-page, check if custom theme is set and return it */
getSubPageTheme() {
@ -63,9 +68,9 @@ const HomeMixin = {
}
},
setTheme() {
const theme = this.getSubPageTheme() || GetTheme();
ApplyLocalTheme(theme);
ApplyCustomVariables(theme);
// const theme = this.getSubPageTheme() || GetTheme();
// ApplyLocalTheme(theme);
// ApplyCustomVariables(theme);
},
updateModalVisibility(modalState) {
this.$store.commit('SET_MODAL_OPEN', modalState);

143
src/mixins/ThemingMixin.js Normal file
View File

@ -0,0 +1,143 @@
/**
* This mixin can be extended by any component or view which needs to manage themes
* It handles fetching and applying themes from the store, updating themes,
* applying custom CSS variables and loading external stylesheets.
* */
import Keys from '@/utils/StoreMutations';
import ErrorHandler from '@/utils/ErrorHandler';
import { builtInThemes, localStorageKeys, mainCssVars } from '@/utils/defaults';
const ThemingMixin = {
data: () => ({
selectedTheme: '', // Used only to bind current them to theme dropdown
}),
computed: {
/* This is the theme from the central store. When it changes, the UI will update */
themeFromStore() {
return this.$store.getters.theme;
},
appConfig() {
return this.$store.getters.appConfig;
},
/* Any extra user-defined themes, to add to dropdown */
extraThemeNames() {
const userThemes = this.appConfig?.cssThemes || [];
if (typeof userThemes === 'string') return [userThemes];
return userThemes;
},
/* If user specified external stylesheet(s), format and return */
externalThemes() {
const availableThemes = {};
if (this.appConfig?.externalStyleSheet) {
const externals = this.appConfig.externalStyleSheet;
if (Array.isArray(externals)) {
externals.forEach((ext, i) => {
availableThemes[`External Stylesheet ${i + 1}`] = ext;
});
} else if (typeof externals === 'string') {
availableThemes['External Stylesheet'] = this.appConfig.externalStyleSheet;
} else {
ErrorHandler('External stylesheets must be of type string or string[]');
}
}
return availableThemes;
},
/* Combines all theme names for dropdown (built-in, user-defined and stylesheets) */
themeNames() {
const externalThemeNames = Object.keys(this.externalThemes);
return [...this.extraThemeNames, ...externalThemeNames, ...builtInThemes];
},
},
watch: {
/* When theme in VueX store changes, then update theme */
themeFromStore(newTheme) {
if (newTheme) {
this.resetToDefault();
this.selectedTheme = newTheme;
this.updateTheme(newTheme);
}
},
},
methods: {
/* Called when user changes theme through the UI
* Updates store, which will in turn update theme through watcher
*/
themeChangedInUI() {
this.$store.commit(Keys.SET_THEME, this.selectedTheme); // Update store
this.updateTheme(this.selectedTheme); // Apply theme to UI
},
/**
* Gets any custom styles the user has applied, wither from local storage, or from the config
* @returns {object} An array of objects, one for each theme, containing kvps for variables
*/
getCustomColors() {
const localColors = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
const configColors = this.appConfig.customColors || {};
return Object.assign(configColors, localColors);
},
/* Gets user custom color preferences for current theme, and applies to DOM */
applyCustomVariables(theme) {
mainCssVars.forEach((vName) => { document.documentElement.style.removeProperty(`--${vName}`); });
const themeColors = this.getCustomColors()[theme];
if (themeColors) {
Object.keys(themeColors).forEach((customVar) => {
document.documentElement.style.setProperty(`--${customVar}`, themeColors[customVar]);
});
}
},
/* Sets the theme, by updating data-theme attribute on the html tag */
applyLocalTheme(newTheme) {
const htmlTag = document.getElementsByTagName('html')[0];
if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme');
htmlTag.setAttribute('data-theme', newTheme);
},
/* If using an external stylesheet, load it in */
applyRemoteTheme(href) {
this.resetToDefault();
const element = document.createElement('link');
element.setAttribute('rel', 'stylesheet');
element.setAttribute('type', 'text/css');
element.setAttribute('id', 'user-defined-stylesheet');
element.setAttribute('href', href);
document.getElementsByTagName('head')[0].appendChild(element);
},
/* Determines if a given theme is local / not a custom user stylesheet */
isThemeLocal(themeToCheck) {
const localThemes = [...builtInThemes, ...this.extraThemeNames];
return localThemes.includes(themeToCheck);
},
/* Updates theme. Checks if the new theme is local or external,
and calls appropriate updating function. Updates local storage */
updateTheme(newTheme) {
if (newTheme.toLowerCase() === 'default') {
this.resetToDefault();
} else if (this.isThemeLocal(newTheme)) {
this.applyLocalTheme(newTheme);
} else if (this.externalThemes[newTheme]) {
this.applyRemoteTheme(this.externalThemes[newTheme]);
}
this.applyCustomVariables(newTheme);
},
/* Removes any applied themes, and deletes any externally loaded stylesheets */
resetToDefault() {
const externalStyles = document.getElementById('user-defined-stylesheet');
if (externalStyles) document.getElementsByTagName('head')[0].removeChild(externalStyles);
document.getElementsByTagName('html')[0].removeAttribute('data-theme');
},
/* Call within mounted hook within a page to apply the correct theme */
initializeTheme() {
const initialTheme = this.themeFromStore;
this.selectedTheme = initialTheme;
const hasExternal = this.externalThemes && Object.entries(this.externalThemes).length > 0;
if (this.isThemeLocal(initialTheme)) {
this.updateTheme(initialTheme);
} else if (hasExternal) {
this.applyRemoteTheme(this.externalThemes[initialTheme]);
}
},
},
};
export default ThemingMixin;

View File

@ -2,7 +2,6 @@
* Mixin that all pre-built and custom widgets extend from.
* Manages loading state, error handling, data updates and user options
*/
import axios from 'axios';
import { Progress } from 'rsup-progress';
import ErrorHandler from '@/utils/ErrorHandler';
import { serviceEndpoints } from '@/utils/defaults';
@ -106,31 +105,68 @@ const WidgetMixin = {
const method = protocol || 'GET';
const url = this.useProxy ? this.proxyReqEndpoint : endpoint;
const data = JSON.stringify(body || {});
const CustomHeaders = options || null;
const headers = this.useProxy
? { 'Target-URL': endpoint, CustomHeaders: JSON.stringify(CustomHeaders) } : CustomHeaders;
const CustomHeaders = options || {};
const headers = new Headers(this.useProxy
? ({ ...CustomHeaders, 'Target-URL': endpoint })
: CustomHeaders);
// If the request is a GET, delete the body
const bodyContent = method.toUpperCase() === 'GET' ? undefined : data;
const timeout = this.options.timeout || this.defaultTimeout;
// Setup Fetch request configuration
const requestConfig = {
method, url, headers, data, timeout,
method,
headers,
body: bodyContent,
signal: undefined, // This will be set below
};
// Make request
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
requestConfig.signal = controller.signal;
// Make request using Fetch API
return new Promise((resolve, reject) => {
axios.request(requestConfig)
.then((response) => {
if (response.data.success === false) {
this.error('Proxy returned error from target server', response.data.message);
fetch(url, requestConfig)
.then(async response => {
const responseData = await response.json();
if (responseData.error) {
this.error('Proxy returned error from target server', responseData.error?.message);
}
resolve(response.data);
if (responseData.success === false) {
this.error('Proxy didn\'t return success from target server', responseData.message);
}
resolve(responseData);
})
.catch((dataFetchError) => {
this.error('Unable to fetch data', dataFetchError);
reject(dataFetchError);
.catch(error => {
if (error.name === 'AbortError') {
this.error('Request timed out', error);
} else {
this.error('Unable to fetch data', error);
}
reject(error);
})
.finally(() => {
clearTimeout(timeoutId);
this.finishLoading();
});
});
},
/* Check if a value is an environment variable, return its value if so. */
parseAsEnvVar(str) {
if (typeof str !== 'string') return str;
if (str.includes('VUE_APP_')) {
const envVar = process.env[str];
if (!envVar) {
this.error(`Environment variable ${str} not found`);
} else {
return envVar;
}
}
return str;
},
},
};

View File

@ -14,23 +14,9 @@ import Home from '@/views/Home.vue';
// Import helper functions, config data and defaults
import { isAuthEnabled, isLoggedIn, isGuestAccessEnabled } from '@/utils/Auth';
import { makePageSlug, makePageName } from '@/utils/ConfigHelpers';
import { metaTagData, startingView, routePaths } from '@/utils/defaults';
import { metaTagData, startingView as defaultStartingView, routePaths } from '@/utils/defaults';
import ErrorHandler from '@/utils/ErrorHandler';
// Import data from users conf file. Note that rebuild is required for this to update.
import conf from '../public/conf.yml';
if (!conf) {
ErrorHandler('You\'ve not got any data in your config file yet.');
}
// Assign top-level config fields, check not null
const config = conf || {};
const pages = config.pages || [];
const pageInfo = config.pageInfo || {};
const appConfig = config.appConfig || {};
Vue.use(Router);
const progress = new Progress({ color: 'var(--progress-bar)' });
@ -42,16 +28,15 @@ const isAuthenticated = () => {
return (!authEnabled || userLoggedIn || guestEnabled);
};
/* Get the users chosen starting view from app config, or return default */
const getStartingView = () => appConfig.startingView || startingView;
// Get the default starting view from environmental variable
const startingView = process.env.VUE_APP_STARTING_VIEW || defaultStartingView;
/**
* Returns the component that should be rendered at the base path,
* Defaults to Home, but the user can change this to Workspace of Minimal
*/
const getStartingComponent = () => {
const usersPreference = getStartingView();
switch (usersPreference) {
switch (startingView) {
case 'minimal': return () => import('./views/Minimal.vue');
case 'workspace': return () => import('./views/Workspace.vue');
default: return Home;
@ -59,71 +44,23 @@ const getStartingComponent = () => {
};
/* Returns the meta tags for each route */
const makeMetaTags = (defaultTitle) => ({
title: pageInfo.title || defaultTitle,
metaTags: metaTagData,
});
const makeSubConfigPath = (rawPath) => {
if (!rawPath) return '';
if (rawPath.startsWith('/') || rawPath.startsWith('http')) return rawPath;
else return `/${rawPath}`;
};
/* For each additional config file, create routes for home, minimal and workspace views */
const makeMultiPageRoutes = (userPages) => {
// If no multi pages specified, or is not array, then return nothing
if (!userPages || !Array.isArray(userPages)) return [];
const multiPageRoutes = [];
// For each user page, create an additional route
userPages.forEach((page) => {
if (!page.name || !page.path) { // Sumin not right, show warning
ErrorHandler('Additional pages must have both a `name` and `path`');
}
// Props to be passed to home mixin
const subPageInfo = {
subPageInfo: {
confPath: makeSubConfigPath(page.path),
pageId: makePageName(page.name),
pageTitle: page.name,
},
};
// Create route for default homepage
multiPageRoutes.push({
path: makePageSlug(page.name, 'home'),
name: `${subPageInfo.subPageInfo.pageId}-home`,
component: Home,
props: subPageInfo,
});
// Create route for the workspace view
multiPageRoutes.push({
path: makePageSlug(page.name, 'workspace'),
name: `${subPageInfo.subPageInfo.pageId}-workspace`,
component: () => import('./views/Workspace.vue'),
props: subPageInfo,
});
// Create route for the minimal view
multiPageRoutes.push({
path: makePageSlug(page.name, 'minimal'),
name: `${subPageInfo.subPageInfo.pageId}-minimal`,
component: () => import('./views/Minimal.vue'),
props: subPageInfo,
});
});
return multiPageRoutes;
const makeMetaTags = (defaultTitle) => {
const userTitle = process.env.VUE_APP_TITLE || '';
const title = userTitle ? `${userTitle} | ${defaultTitle}` : defaultTitle;
return { title, metaTags: metaTagData };
};
/* Routing mode, can be either 'hash', 'history' or 'abstract' */
const mode = appConfig.routingMode || 'history';
const mode = process.env.VUE_APP_ROUTING_MODE || 'history';
/* List of all routes, props, components and metadata */
const router = new Router({
mode,
routes: [
...makeMultiPageRoutes(pages),
// ...makeMultiPageRoutes(pages),
{ // The default view can be customized by the user
path: '/',
name: `landing-page-${getStartingView()}`,
name: `landing-page-${startingView}`,
component: getStartingComponent(),
meta: makeMetaTags('Home Page'),
},
@ -197,7 +134,7 @@ const router = new Router({
* if so, then ensure that they are correctly logged in as a valid user
* If not logged in, prevent all access and redirect them to login page
* */
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from, next) => {
progress.start();
if (to.name !== 'login' && !isAuthenticated()) next({ name: 'login' });
else next();

View File

@ -4,22 +4,22 @@ import Vuex from 'vuex';
import axios from 'axios';
import yaml from 'js-yaml';
import Keys from '@/utils/StoreMutations';
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { componentVisibility } from '@/utils/ConfigHelpers';
import { makePageName, formatConfigPath, componentVisibility } from '@/utils/ConfigHelpers';
import { applyItemId } from '@/utils/SectionHelpers';
import filterUserSections from '@/utils/CheckSectionVisibility';
import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
import { isUserAdmin } from '@/utils/Auth';
import { localStorageKeys } from './utils/defaults';
import { localStorageKeys, theme as defaultTheme } from './utils/defaults';
Vue.use(Vuex);
const {
INITIALIZE_CONFIG,
INITIALIZE_MULTI_PAGE_CONFIG,
INITIALIZE_ROOT_CONFIG,
SET_CONFIG,
SET_REMOTE_CONFIG,
SET_CURRENT_SUB_PAGE,
SET_ROOT_CONFIG,
SET_CURRENT_CONFIG_INFO,
SET_IS_USING_LOCAL_CONFIG,
SET_MODAL_OPEN,
SET_LANGUAGE,
SET_ITEM_LAYOUT,
@ -45,11 +45,12 @@ const {
const store = new Vuex.Store({
state: {
config: {}, // The current config, rendered to the UI
remoteConfig: {}, // The configuration stored on the server
config: {}, // The current config being used, and rendered to the UI
rootConfig: null, // Always the content of main config file, never used directly
editMode: false, // While true, the user can drag and edit items + sections
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
currentConfigInfo: undefined, // For multi-page support, will store info about config file
currentConfigInfo: {}, // For multi-page support, will store info about config file
isUsingLocalConfig: false, // If true, will use local config instead of fetched
navigateConfToTab: undefined, // Used to switch active tab in config modal
},
getters: {
@ -68,17 +69,14 @@ const store = new Vuex.Store({
return filterUserSections(state.config.sections || []);
},
pages(state) {
return state.remoteConfig.pages || [];
return state.config.pages || [];
},
theme(state) {
let localTheme = null;
if (state.currentConfigInfo?.pageId) {
const themeStoreKey = `${localStorageKeys.THEME}-${state.currentConfigInfo?.pageId}`;
localTheme = localStorage[themeStoreKey];
} else {
localTheme = localStorage[localStorageKeys.THEME];
}
return localTheme || state.config.appConfig.theme;
const localStorageKey = state.currentConfigInfo.confId
? `${localStorageKeys.THEME}-${state.currentConfigInfo.confId}` : localStorageKeys.THEME;
const localTheme = localStorage[localStorageKey];
// Return either theme from local storage, or from appConfig
return localTheme || state.config.appConfig.theme || defaultTheme;
},
webSearch(state, getters) {
return getters.appConfig.webSearch || {};
@ -146,14 +144,21 @@ const store = new Vuex.Store({
},
},
mutations: {
/* Set the master config */
[SET_ROOT_CONFIG](state, config) {
if (!config.appConfig) config.appConfig = {};
state.config = config;
},
/* The config to display and edit. Will differ from ROOT_CONFIG when using multi-page */
[SET_CONFIG](state, config) {
if (!config.appConfig) config.appConfig = {};
state.config = config;
},
[SET_REMOTE_CONFIG](state, config) {
const notNullConfig = config || {};
if (!notNullConfig.appConfig) notNullConfig.appConfig = {};
state.remoteConfig = notNullConfig;
[SET_CURRENT_CONFIG_INFO](state, subConfigInfo) {
state.currentConfigInfo = subConfigInfo;
},
[SET_IS_USING_LOCAL_CONFIG](state, isUsingLocalConfig) {
state.isUsingLocalConfig = isUsingLocalConfig;
},
[SET_LANGUAGE](state, lang) {
const newConfig = state.config;
@ -276,12 +281,13 @@ const store = new Vuex.Store({
config.sections = applyItemId(config.sections);
state.config = config;
},
[SET_THEME](state, themOps) {
const { theme, pageId } = themOps;
[SET_THEME](state, theme) {
const newConfig = { ...state.config };
newConfig.appConfig.theme = theme;
state.config = newConfig;
const themeStoreKey = pageId ? `${localStorageKeys.THEME}-${pageId}` : localStorageKeys.THEME;
const pageId = state.currentConfigInfo.confId;
const themeStoreKey = pageId
? `${localStorageKeys.THEME}-${pageId}` : localStorageKeys.THEME;
localStorage.setItem(themeStoreKey, theme);
InfoHandler('Theme updated', InfoKeys.VISUAL);
},
@ -292,15 +298,11 @@ const store = new Vuex.Store({
InfoHandler('Color palette updated', InfoKeys.VISUAL);
},
[SET_ITEM_LAYOUT](state, layout) {
const newConfig = { ...state.config };
newConfig.appConfig.layout = layout;
state.config = newConfig;
state.config.appConfig.layout = layout;
InfoHandler('Layout updated', InfoKeys.VISUAL);
},
[SET_ITEM_SIZE](state, iconSize) {
const newConfig = { ...state.config };
newConfig.appConfig.iconSize = iconSize;
state.config = newConfig;
state.config.appConfig.iconSize = iconSize;
InfoHandler('Item size updated', InfoKeys.VISUAL);
},
[UPDATE_CUSTOM_CSS](state, customCss) {
@ -310,42 +312,94 @@ const store = new Vuex.Store({
[CONF_MENU_INDEX](state, index) {
state.navigateConfToTab = index;
},
[SET_CURRENT_SUB_PAGE](state, subPageObject) {
if (!subPageObject) {
// Set theme back to primary when navigating to index page
const defaulTheme = localStorage.getItem(localStorageKeys.PRIMARY_THEME);
if (defaulTheme) state.config.appConfig.theme = defaulTheme;
}
state.currentConfigInfo = subPageObject;
},
[USE_MAIN_CONFIG](state) {
if (state.remoteConfig) {
state.config = state.remoteConfig;
} else {
this.dispatch(Keys.INITIALIZE_CONFIG);
}
/* Set config to rootConfig, by calling initialize with no params */
async [USE_MAIN_CONFIG]() {
this.dispatch(Keys.INITIALIZE_CONFIG);
},
},
actions: {
/* Called when app first loaded. Reads config and sets state */
async [INITIALIZE_CONFIG]({ commit }) {
// Get the config file from the server and store it for use by the accumulator
commit(SET_REMOTE_CONFIG, yaml.load((await axios.get('/conf.yml')).data));
const deepCopy = (json) => JSON.parse(JSON.stringify(json));
const config = deepCopy(new ConfigAccumulator().config());
commit(SET_CONFIG, config);
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
// Load and parse config from root config file
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
const data = await yaml.load((await axios.get(configFilePath)).data);
// Replace missing root properties with empty objects
if (!data.appConfig) data.appConfig = {};
if (!data.pageInfo) data.pageInfo = {};
if (!data.sections) data.sections = [];
// Set the state, and return data
commit(SET_ROOT_CONFIG, data);
return data;
},
/* Fetch config for a sub-page (sections and pageInfo only) */
async [INITIALIZE_MULTI_PAGE_CONFIG]({ commit, state }, configPath) {
axios.get(configPath).then((response) => {
const subConfig = yaml.load(response.data);
const pageTheme = subConfig.appConfig?.theme;
subConfig.appConfig = state.config.appConfig; // Always use parent appConfig
if (pageTheme) subConfig.appConfig.theme = pageTheme; // Apply page theme override
commit(SET_CONFIG, subConfig);
}).catch((err) => {
ErrorHandler(`Unable to load config from '${configPath}'`, err);
});
/**
* Fetches config and updates state
* If not on sub-page, will trigger the fetch of main config, then use that
* If using sub-page config, then fetch that sub-config, then
* override certain fields (appConfig, pages) and update config
*/
async [INITIALIZE_CONFIG]({ commit, state }, subConfigId) {
const rootConfig = state.rootConfig || await this.dispatch(Keys.INITIALIZE_ROOT_CONFIG);
commit(SET_IS_USING_LOCAL_CONFIG, false);
if (!subConfigId) { // Use root config as config
commit(SET_CONFIG, rootConfig);
commit(SET_CURRENT_CONFIG_INFO, {});
let localSections = [];
const localSectionsRaw = localStorage[localStorageKeys.CONF_SECTIONS];
if (localSectionsRaw) {
try {
const json = JSON.parse(localSectionsRaw);
if (json.length >= 1) localSections = json;
} catch (e) {
ErrorHandler('Malformed section data in local storage');
}
}
if (localSections.length > 0) {
rootConfig.sections = localSections;
commit(SET_IS_USING_LOCAL_CONFIG, true);
}
return rootConfig;
} else {
// Find and format path to fetch sub-config from
const subConfigPath = formatConfigPath(rootConfig?.pages?.find(
(page) => makePageName(page.name) === subConfigId,
)?.path);
if (!subConfigPath) {
ErrorHandler(`Unable to find config for '${subConfigId}'`);
return null;
}
axios.get(subConfigPath).then((response) => {
// Parse the YAML
const configContent = yaml.load(response.data) || {};
// Certain values must be inherited from root config
const theme = configContent?.appConfig?.theme || rootConfig.appConfig?.theme || 'default';
configContent.appConfig = rootConfig.appConfig;
configContent.pages = rootConfig.pages;
configContent.appConfig.theme = theme;
// Load local sections if they exist
const localSectionsRaw = localStorage[`${localStorageKeys.CONF_SECTIONS}-${subConfigId}`];
if (localSectionsRaw) {
try {
const json = JSON.parse(localSectionsRaw);
if (json.length >= 1) {
configContent.sections = json;
commit(SET_IS_USING_LOCAL_CONFIG, true);
}
} catch (e) {
ErrorHandler('Malformed section data in local storage for sub-config');
}
}
// Set the config
commit(SET_CONFIG, configContent);
commit(SET_CURRENT_CONFIG_INFO, { confPath: subConfigPath, confId: subConfigId });
}).catch((err) => {
ErrorHandler(`Unable to load config from '${subConfigPath}'`, err);
});
}
return null;
},
},
modules: {},

View File

@ -1619,6 +1619,229 @@ html[data-theme='lissy'] {
}
}
html[data-theme='glass'],
html[data-theme='glass-2'],
html[data-theme='neomorphic'] {
--primary: #fff;
--item-group-outer-background: rgba(0, 0, 0, 0.25);
--item-group-background: transparent;
--item-group-heading-text-color: #fff;
--item-group-heading-text-color-hover: #ffffffd6;
--item-group-shadow: 5px 2px 20px rgba(0, 0, 0, 0.5);
--background: #190842;
--background-darker: #190842;
--settings-background: transparent;
--search-container-background: transparent;
--font-headings: 'Segoe UI', 'Ariel', 'sans-serif';
--font-body: 'Roboto', 'Segoe UI', 'Ariel', 'sans-serif';
--minimal-view-background-color: transparent;
--minimal-view-group-background: rgba(255, 255, 255, 0.15);
--minimal-view-section-heading-background: rgba(255, 255, 255, 0.15);
--minimal-view-section-heading-color: rgba(255, 255, 255, 0.15);
--config-settings-background: #16073de3;
--cloud-backup-background: #16073de3;
@mixin item-transition-styles($bg: transparent, $hover-bg: rgba(255, 255, 255, 0.15), $hover-shadow: rgba(0, 0, 0, 0.75)) {
background: $bg;
border: 1px solid transparent;
box-shadow: none;
transition: 0.2s all ease-in-out;
&:hover {
border-radius: 0.35rem;
box-shadow: 0 4px 30px $hover-shadow;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.19);
background: $hover-bg;
}
}
@mixin transform-scale($normal-scale: 1, $hover-scale: 1.25) {
transition: 0.1s all ease-in-out;
transform: scale($normal-scale);
&:hover {
transform: scale($hover-scale);
}
}
body {
background-size: cover;
background-color: #090317;
.home {
background: transparent;
}
}
.settings-outer, header, .dashy-modal, .dashy-modal .tabs {
background: transparent;
// backdrop-filter: blur(4px);
}
// Minimal view components
.minimal-section-inner, div.minimal-section-heading {
backdrop-filter: blur(10px);
border: 1px solid rgba(145, 145, 145, 0.45);
border-bottom: none;
&.selected {
border: 1px solid rgba(145, 145, 145, 0.45);
background: var(--minimal-view-group-background);
}
}
.minimal-section-heading {
color: var(--minimal-view-section-heading-background);
&.selected {
.section-icon, .section-title {
color: var(--primary) !important;
}
}
}
--glass-button-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
--glass-button-hover-shadow: 2px 2px 5px rgba(0, 0, 0, 0.7);
// Forms and inputs
button.save-button,
.action-buttons button,
.cloud-backup-restore-wrapper button,
.tab__nav__item,
div.input-container input.input-field,
form.normal input,
.nav-outer nav .nav-item,
div.edit-mode-bottom-banner .edit-banner-section button,
.v-select.theme-dropdown.vs__dropdown-toggle,
.theme-dropdown div.vs__dropdown-toggle,
.config-buttons > svg,
.display-options svg,
form.minimal input,
a.config-button, button.config-button {
border-radius: 0.35rem;
box-shadow: var(--glass-button-shadow);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.19);
background: rgba(255, 255, 255, 0.15);
transition: all 0.2s ease-in-out;
&:hover, &.selected {
box-shadow: var(--glass-button-hover-shadow);
border: 1px solid rgba(255, 255, 255, 0.25) !important;
background: #ffffff42 !important;
color: var(--primary) !important;
path { fill: var(--primary); }
}
}
.tab__nav__items {
gap: 1rem;
margin: 0.5rem 0 0;
.tab__nav__item {
padding: 0.5rem 0.5rem;
&:hover, .active, .active:hover {
background: #ffffff42 !important;
span { color: var(--primary) !important; }
}
}
}
.main-options-container .config-buttons, div.cloud-backup-restore-wrapper {
background: none;
}
// Item and collapsable specific styles
.item {
@include item-transition-styles(transparent, rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.75));
.item-icon {
@include transform-scale(1.1, 1.25);
}
}
.collapsable {
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.45);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
// Modal specific styles
.dashy-modal {
box-shadow: 0 20px 40px -2px #000000b8, 1px 1px 6px #000000a6 !important;
}
.tab-item {
background: var(--config-settings-background);
}
.theme-configurator-wrapper, .view-switcher {
backdrop-filter: blur(10px);
background: var(--config-settings-background);
border: 1px solid rgba(255, 255, 255, 0.19);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
.edit-mode-top-banner {
backdrop-filter: blur(10px);
background: #ffffff6b;
border-bottom: 1px solid black;
span { color: #eaff9d; }
}
div.edit-mode-bottom-banner, .add-new-section {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(50px);
}
}
html[data-theme='glass'] {
body {
background: url('https://zeabur.com/images/bg.png') center center no-repeat;
background-size: cover;
background-color: #090317;
.home {
background: transparent;
}
}
}
html[data-theme='glass-2'] {
body {
background: url('https://i.ibb.co/FnLH6bj/dashy-glass.jpg') center center no-repeat;
background-size: cover;
background-color: #090317;
}
}
html[data-theme='neomorphic'] {
--primary: #fff;
--item-group-outer-background: rgba(255, 255, 255, 0.15);
--item-group-background: transparent;
--item-group-heading-text-color: #fff;
--item-group-shadow: 5px 2px 20px rgba(0, 0, 0, 0.5);
--background: #5b56f7;
// --background: #4bdbfd;
--background-darker: #12103c;
--settings-background: transparent;
--search-container-background: transparent;
--font-headings: 'Segoe UI', 'Ariel', 'sans-serif';
--font-body: 'Roboto', 'Segoe UI', 'Ariel', 'sans-serif';
--minimal-view-background-color: transparent;
--minimal-view-group-background: rgba(255, 255, 255, 0.15);
--minimal-view-section-heading-background: rgba(255, 255, 255, 0.15);
--minimal-view-section-heading-color: rgba(255, 255, 255, 0.15);
--config-settings-background: #1fb8f4e3;
--cloud-backup-background: #16073de3;
--glass-button-shadow: 0px 1px 5px rgba(0, 0, 0, 0.5);
--glass-button-hover-shadow: 2px 2px 5px rgba(0, 0, 0, 0.7);
body {
background: var(--background);
}
.item:hover { box-shadow: 0 3px 10px rgba(0, 0, 0, 0.5); }
.collapsable { border: 1px solid rgba(255, 255, 255, 0.25) !important; }
}
html[data-theme='cherry-blossom'] {
--primary: #e1e8ee;
--background: #11171d;

View File

@ -11,6 +11,8 @@ const getAppConfig = () => {
return config.appConfig || {};
};
// const appConfig = $store.getters.appConfig || {};
/**
* Called when the user is still using array for users, prints warning
* This was a breaking change, implemented in V 1.6.5

View File

@ -16,11 +16,9 @@ import ErrorHandler from '@/utils/ErrorHandler';
import { applyItemId } from '@/utils/SectionHelpers';
import $store from '@/store';
import buildConf from '../../public/conf.yml';
export default class ConfigAccumulator {
constructor() {
this.conf = $store.state.remoteConfig;
this.conf = $store.state.config;
}
pages() {
@ -33,8 +31,6 @@ export default class ConfigAccumulator {
// Set app config from file
if (this.conf && this.conf.appConfig) {
appConfigFile = this.conf.appConfig;
} else if (buildConf && buildConf.appConfig) {
appConfigFile = buildConf.appConfig;
}
// Fill in defaults if anything missing
let usersAppConfig = defaultAppConfig;

View File

@ -1,10 +1,10 @@
import ConfigAccumulator from '@/utils/ConfigAccumalator';
// import $store from '@/store';
import filterUserSections from '@/utils/CheckSectionVisibility';
import { languages } from '@/utils/languages';
import {
visibleComponents,
localStorageKeys,
theme as defaultTheme,
language as defaultLanguage,
} from '@/utils/defaults';
import ErrorHandler from '@/utils/ErrorHandler';
@ -26,6 +26,13 @@ export const makePageSlug = (pageName, pageType) => {
return `/${pageType}/${formattedName}`;
};
/* Put fetch path for additional configs in correct format */
export const formatConfigPath = (configPath) => {
if (configPath.includes('http')) return configPath;
if (configPath.substring(0, 1) !== '/') return `/${configPath}`;
return configPath;
};
/**
* Initiates the Accumulator class and generates a complete config object
* Self-executing function, returns the full user config as a JSON object
@ -67,35 +74,12 @@ export const componentVisibility = (appConfig) => {
};
};
/**
* Gets the users saved theme, first looks for local storage theme,
* then looks at user's appConfig, and finally checks the defaults
* @returns {string} Name of theme to apply
*/
export const getTheme = () => {
const localTheme = localStorage[localStorageKeys.THEME];
const appConfigTheme = config.appConfig.theme;
return localTheme || appConfigTheme || defaultTheme;
};
/**
* Gets any custom styles the user has applied, wither from local storage, or from the config
* @returns {object} An array of objects, one for each theme, containing kvps for variables
*/
export const getCustomColors = () => {
const localColors = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
const configColors = config.appConfig.customColors || {};
return Object.assign(configColors, localColors);
};
/**
* Returns a list of items which the user has assigned a hotkey to
* So that when the hotkey is pressed, the app/ service can be launched
*/
export const getCustomKeyShortcuts = () => {
const Accumulator = new ConfigAccumulator();
export const getCustomKeyShortcuts = (sections) => {
const results = [];
const sections = filterUserSections(Accumulator.sections()) || [];
sections.forEach((section) => {
const itemsWithHotKeys = section.items.filter(item => item.hotkey);
results.push(itemsWithHotKeys.map(item => ({ hotkey: item.hotkey, url: item.url })));

View File

@ -534,6 +534,37 @@
}
}
},
"enableHeaderAuth": {
"title": "Enable HeaderAuth?",
"type": "boolean",
"default": false,
"description": "If set to true, enable Header Authentication. See appConfig.auth.headerAuth"
},
"headerAuth": {
"type": "object",
"description": "Configuration for headerAuth",
"additionalProperties": false,
"required": [
"proxyWhitelist"
],
"properties": {
"userHeader": {
"title": "User Header",
"type": "string",
"description": "Header name which contains username",
"default": "REMOTE_USER"
},
"proxyWhitelist": {
"title": "Upstream Proxy Auth Trust",
"type": "array",
"description": "Upstream proxy servers to expect authenticated requests from",
"items": {
"type": "string",
"description": "IPs of upstream proxies that will be trusted"
}
}
}
},
"enableKeycloak": {
"title": "Enable Keycloak?",
"type": "boolean",

View File

@ -9,12 +9,12 @@
/* eslint-disable global-require */
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import $store from '@/store';
import { sentryDsn } from '@/utils/defaults';
const ErrorReporting = (Vue, router) => {
// Fetch users config
const appConfig = new ConfigAccumulator().appConfig() || {};
const appConfig = $store.getters.appConfig || {};
// Check if error reporting is enabled. Only proceed if user has turned it on.
if (appConfig.enableErrorReporting) {
// Get current app version

78
src/utils/HeaderAuth.js Normal file
View File

@ -0,0 +1,78 @@
import axios from 'axios';
import sha256 from 'crypto-js/sha256';
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { cookieKeys, localStorageKeys, serviceEndpoints } from '@/utils/defaults';
import { InfoHandler, ErrorHandler, InfoKeys } from '@/utils/ErrorHandler';
import { logout } from '@/utils/Auth';
const getAppConfig = () => {
const Accumulator = new ConfigAccumulator();
const config = Accumulator.config();
return config.appConfig || {};
};
class HeaderAuth {
constructor() {
const { auth } = getAppConfig();
const {
userHeader, proxyWhitelist,
} = auth.headerAuth;
this.userHeader = userHeader;
this.proxyWhitelist = proxyWhitelist;
this.users = auth.users;
}
/* eslint-disable class-methods-use-this */
login() {
return new Promise((resolve, reject) => {
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
axios.get(`${baseUrl}${serviceEndpoints.getUser}`).then((response) => {
if (!response.data) {
reject(Error('Error, expected data nout returned'));
} else if (response.data.errorMsg) {
reject(response.data.errorMsg);
} else {
try {
this.users.forEach((user) => {
if (user.user.toLowerCase() === response.data.user.toLowerCase()) { // User found
const strAndUpper = (input) => input.toString().toUpperCase();
const sha = strAndUpper(sha256(strAndUpper(user.user) + strAndUpper(user.hash)));
document.cookie = `${cookieKeys.AUTH_TOKEN}=${sha};`;
localStorage.setItem(localStorageKeys.USERNAME, user.user);
InfoHandler(`Successfully signed in as ${response.data.user}`, InfoKeys.AUTH);
resolve(response.data.user);
}
});
} catch (e) {
reject(e);
}
}
});
});
}
logout() {
logout();
}
}
export const isHeaderAuthEnabled = () => {
const { auth } = getAppConfig();
if (!auth) return false;
return auth.enableHeaderAuth || false;
};
let headerAuth;
export const initHeaderAuth = () => {
headerAuth = new HeaderAuth();
return headerAuth.login();
};
// TODO: Find where this is implemented
export const getHeaderAuth = () => {
if (!headerAuth) {
ErrorHandler("HeaderAuth not initialized, can't get instance of class");
}
return headerAuth;
};

View File

@ -1,9 +1,12 @@
// A list of mutation names
const KEY_NAMES = [
'INITIALIZE_CONFIG',
'INITIALIZE_ROOT_CONFIG',
'INITIALIZE_MULTI_PAGE_CONFIG',
'SET_CONFIG',
'SET_REMOTE_CONFIG',
'SET_ROOT_CONFIG',
'SET_CURRENT_CONFIG_INFO',
'SET_IS_USING_LOCAL_CONFIG',
'SET_CURRENT_SUB_PAGE',
'SET_MODAL_OPEN',
'SET_LANGUAGE',

View File

@ -1,72 +0,0 @@
import ErrorHandler from '@/utils/ErrorHandler';
import { getTheme, getCustomColors } from '@/utils/ConfigHelpers';
import { mainCssVars } from '@/utils/defaults';
/* Returns users current theme */
export const GetTheme = () => getTheme();
/* Gets user custom color preferences for current theme, and applies to DOM */
export const ApplyCustomVariables = (theme) => {
mainCssVars.forEach((vName) => { document.documentElement.style.removeProperty(`--${vName}`); });
const themeColors = getCustomColors()[theme];
if (themeColors) {
Object.keys(themeColors).forEach((customVar) => {
document.documentElement.style.setProperty(`--${customVar}`, themeColors[customVar]);
});
}
};
/* Sets the theme, by updating data-theme attribute on the html tag */
export const ApplyLocalTheme = (newTheme) => {
const htmlTag = document.getElementsByTagName('html')[0];
if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme');
htmlTag.setAttribute('data-theme', newTheme);
};
/**
* A function for pre-loading, and easy switching of external stylesheets
* External CSS is preloaded to avoid FOUC
*/
export const LoadExternalTheme = function th() {
/* Preload selected external theme */
const preloadTheme = (href) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = href;
document.head.appendChild(link);
return new Promise((resolve, reject) => {
link.onload = e => {
const { sheet } = e.target;
sheet.disabled = true;
resolve(sheet);
};
link.onerror = reject;
});
};
/* Check theme is selected, and it exists */
const checkTheme = (themes, name) => {
if ((!name) || (name !== 'custom' && !themes[name])) {
ErrorHandler(`Theme: '${name || '[not selected]'}' does not exist.`);
return false;
}
return true;
};
/* Disable all but selected theme */
const selectTheme = (themes, name) => {
if (checkTheme(themes, name)) {
const t = themes; // To avoid ESLint complaining about mutating a param
Object.keys(themes).forEach(n => { t[n].disabled = (n !== name); });
}
};
const themes = {};
return {
add(name, href) { return preloadTheme(href).then(s => { themes[name] = s; }); },
set theme(name) { selectTheme(themes, name); },
get theme() { return Object.keys(themes).find(n => !themes[n].disabled); },
};
};

View File

@ -28,9 +28,9 @@ module.exports = {
openingMethod: 'newtab',
/* The page paths for each route within the app for the router */
routePaths: {
home: '/home',
minimal: '/minimal',
workspace: '/workspace',
home: '/home/:config?/',
minimal: '/minimal/:config?/',
workspace: '/workspace/:config?/',
about: '/about',
login: '/login',
download: '/download',
@ -44,10 +44,12 @@ module.exports = {
rebuild: '/config-manager/rebuild',
systemInfo: '/system-info',
corsProxy: '/cors-proxy',
getUser: '/get-user',
},
/* List of built-in themes, to be displayed within the theme-switcher dropdown */
builtInThemes: [
'default',
'glass',
'callisto',
'material',
'material-dark',
@ -85,6 +87,8 @@ module.exports = {
'adventure-basic',
'basic',
'tama',
'neomorphic',
'glass-2',
],
/* Default color options for the theme configurator swatches */
swatches: [
@ -112,7 +116,7 @@ module.exports = {
/* Key names for local storage identifiers */
localStorageKeys: {
LANGUAGE: 'language',
HIDE_WELCOME_BANNER: 'hideWelcomeHelpers',
HIDE_INFO_NOTIFICATION: 'hideWelcomeHelpers',
LAYOUT_ORIENTATION: 'layoutOrientation',
COLLAPSE_STATE: 'collapseState',
ICON_SIZE: 'iconSize',
@ -120,6 +124,7 @@ module.exports = {
PRIMARY_THEME: 'primaryTheme',
CUSTOM_COLORS: 'customColors',
CONF_SECTIONS: 'confSections',
CONF_PAGES: 'confPages',
CONF_WIDGETS: 'confSections',
PAGE_INFO: 'pageInfo',
APP_CONFIG: 'appConfig',
@ -184,7 +189,7 @@ module.exports = {
// delay: { show: 380, hide: 0 },
},
/* Server location of the Backup & Sync cloud function */
backupEndpoint: 'https://dashy-sync-service.as93.net',
backupEndpoint: 'https://sync-service.dashy.to',
/* Available services for fetching favicon icon for user apps */
faviconApiEndpoints: {
allesedv: 'https://f1.allesedv.com/128/$URL',

View File

@ -56,6 +56,8 @@
<EditModeSaveMenu v-if="isEditMode" />
<!-- Modal for viewing and exporting configuration file -->
<ExportConfigMenu />
<!-- Shows pertinent info -->
<NotificationThing v-if="$store.state.isUsingLocalConfig"/>
</div>
</template>
@ -66,6 +68,7 @@ import Section from '@/components/LinkItems/Section.vue';
import EditModeSaveMenu from '@/components/InteractiveEditor/EditModeSaveMenu.vue';
import ExportConfigMenu from '@/components/InteractiveEditor/ExportConfigMenu.vue';
import AddNewSection from '@/components/InteractiveEditor/AddNewSectionLauncher.vue';
import NotificationThing from '@/components/Settings/LocalConfigWarning.vue';
import StoreKeys from '@/utils/StoreMutations';
import { localStorageKeys, modalNames } from '@/utils/defaults';
import ErrorHandler from '@/utils/ErrorHandler';
@ -79,6 +82,7 @@ export default {
EditModeSaveMenu,
ExportConfigMenu,
AddNewSection,
NotificationThing,
Section,
BackIcon,
},
@ -119,12 +123,16 @@ export default {
},
watch: {
layoutOrientation(layout) {
localStorage.setItem(localStorageKeys.LAYOUT_ORIENTATION, layout);
this.layout = layout;
if (layout) {
localStorage.setItem(localStorageKeys.LAYOUT_ORIENTATION, layout);
this.layout = layout;
}
},
iconSize(size) {
localStorage.setItem(localStorageKeys.ICON_SIZE, size);
this.itemSizeBound = size;
if (size) {
localStorage.setItem(localStorageKeys.ICON_SIZE, size);
this.itemSizeBound = size;
}
},
},
methods: {

View File

@ -17,7 +17,7 @@
:class="`item-group-container ${!tabbedView ? 'showing-all' : ''}`">
<!-- Section heading buttons -->
<MinimalHeading
v-for="(section, index) in getSections(sections)"
v-for="(section, index) in sections"
:key="`heading-${index}`"
:index="index"
:title="section.name"
@ -29,7 +29,7 @@
/>
<!-- Section item groups -->
<MinimalSection
v-for="(section, index) in getSections(sections)"
v-for="(section, index) in sections"
:key="`body-${index}`"
:index="index"
:title="section.name"
@ -57,7 +57,6 @@ import HomeMixin from '@/mixins/HomeMixin';
import MinimalSection from '@/components/MinimalView/MinimalSection.vue';
import MinimalHeading from '@/components/MinimalView/MinimalHeading.vue';
import MinimalSearch from '@/components/MinimalView/MinimalSearch.vue';
import { localStorageKeys } from '@/utils/defaults';
import ConfigLauncher from '@/components/Settings/ConfigLauncher';
export default {
@ -83,17 +82,6 @@ export default {
sectionSelected(index) {
this.selectedSection = index;
},
/* Returns sections from local storage if available, otherwise uses the conf.yml */
getSections(sections) {
// If the user has stored sections in local storage, return those
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
if (localSections) {
const json = JSON.parse(localSections);
if (json.length >= 1) return json;
}
// Otherwise, return the usuall data from conf.yml
return sections;
},
/* Clears input field, once a searched item is opened */
finishedSearching() {
if (this.$refs.filterComp) this.$refs.filterComp.clearMinFilterInput();

View File

@ -19,7 +19,6 @@ import WebContent from '@/components/Workspace/WebContent';
import WidgetView from '@/components/Workspace/WidgetView';
import MultiTaskingWebComtent from '@/components/Workspace/MultiTaskingWebComtent';
import Defaults from '@/utils/defaults';
import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper';
export default {
name: 'Workspace',
@ -27,9 +26,6 @@ export default {
data: () => ({
url: '',
widgets: null,
GetTheme,
ApplyLocalTheme,
ApplyCustomVariables,
}),
computed: {
sections() {

39
tsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": false,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest",
"node"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.vue",
"tests/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@ -1,47 +1,47 @@
---
# Page meta info, like heading, footer text and nav links
pageInfo:
title: Dashy
description: Welcome to your new dashboard!
navLinks:
- title: GitHub
path: https://github.com/Lissy93/dashy
- title: Documentation
path: https://dashy.to/docs
# Optional app settings and configuration
appConfig:
theme: colorful
# Main content - An array of sections, each containing an array of items
sections:
- name: Getting Started
icon: fas fa-rocket
items:
- title: Dashy Live
description: Development a project management links for Dashy
icon: https://i.ibb.co/qWWpD0v/astro-dab-128.png
url: https://live.dashy.to/
target: newtab
- title: GitHub
description: Source Code, Issues and Pull Requests
url: https://github.com/lissy93/dashy
icon: favicon
- title: Docs
description: Configuring & Usage Documentation
provider: Dashy.to
icon: far fa-book
url: https://dashy.to/docs
- title: Showcase
description: See how others are using Dashy
url: https://github.com/Lissy93/dashy/blob/master/docs/showcase.md
icon: far fa-grin-hearts
- title: Config Guide
description: See full list of configuration options
url: https://github.com/Lissy93/dashy/blob/master/docs/configuring.md
icon: fas fa-wrench
- title: Support
description: Get help with Dashy, raise a bug, or get in contact
url: https://github.com/Lissy93/dashy/blob/master/.github/SUPPORT.md
icon: far fa-hands-helping
---
# Page meta info, like heading, footer text and nav links
pageInfo:
title: Dashy
description: Welcome to your new dashboard!
navLinks:
- title: GitHub
path: https://github.com/Lissy93/dashy
- title: Documentation
path: https://dashy.to/docs
# Optional app settings and configuration
appConfig:
theme: colorful
# Main content - An array of sections, each containing an array of items
sections:
- name: Getting Started
icon: fas fa-rocket
items:
- title: Dashy Live
description: Development a project management links for Dashy
icon: https://i.ibb.co/qWWpD0v/astro-dab-128.png
url: https://live.dashy.to/
target: newtab
- title: GitHub
description: Source Code, Issues and Pull Requests
url: https://github.com/lissy93/dashy
icon: favicon
- title: Docs
description: Configuring & Usage Documentation
provider: Dashy.to
icon: far fa-book
url: https://dashy.to/docs
- title: Showcase
description: See how others are using Dashy
url: https://github.com/Lissy93/dashy/blob/master/docs/showcase.md
icon: far fa-grin-hearts
- title: Config Guide
description: See full list of configuration options
url: https://github.com/Lissy93/dashy/blob/master/docs/configuring.md
icon: fas fa-wrench
- title: Support
description: Get help with Dashy, raise a bug, or get in contact
url: https://github.com/Lissy93/dashy/blob/master/.github/SUPPORT.md
icon: far fa-hands-helping

View File

@ -1,9 +1,26 @@
/**
* Global config for the main Vue app. ES7 not supported here.
* See docs for all config options: https://cli.vuejs.org/config
* Dashy is built using Vue (2). This is the main Vue and Webpack configuration
*
* User Configurable Options:
* - NODE_ENV: Sets the app mode (production, development, test).
* - BASE_URL: Root URL for the app deployment (defaults to '/').
* - INTEGRITY: Enables SRI, set to 'true' to activate.
* - USER_DATA_DIR: Sets an alternative dir for user data (defaults ./user-data).
* - IS_DOCKER: Indicates if running in a Docker container.
* - IS_SERVER: Indicates if running as a server (as opposed to static build).
*
* Documentation:
* - Vue CLI Config options: https://cli.vuejs.org/config
* - For Dashy docs, see the repo: https://github.com/lissy93/dashy
*
* Note: ES7 syntax is not supported in this configuration context.
* Licensed under the MIT License, (C) Alicia Sykes 2024 (see LICENSE for details).
*/
// Get app mode: production, development or test
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// Get app mode: production, development, or test
const mode = process.env.NODE_ENV || 'production';
// Get current version
@ -18,14 +35,46 @@ const publicPath = process.env.BASE_URL || '/';
// Should enable Subresource Integrity (SRI) on link and script tags
const integrity = process.env.INTEGRITY === 'true';
// If neither env vars are set, then it's a static build
const isServer = process.env.IS_DOCKER || process.env.IS_SERVER || false;
// Use copy-webpack-plugin to copy user-data to dist IF not running as a server
const plugins = !isServer ? [
new CopyWebpackPlugin({
patterns: [
{ from: './user-data', to: './' },
],
}),
] : [];
// Webpack Config
const configureWebpack = {
mode,
plugins,
module: {
rules: [
{ test: /.svg$/, loader: 'vue-svg-loader' },
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: { appendTsSuffixTo: [/\.vue$/] },
},
],
},
performance: {
maxEntrypointSize: 10000000,
maxAssetSize: 10000000,
},
};
// Development server config
const devServer = {
contentBase: [
path.join(__dirname, 'public'),
path.join(__dirname, process.env.USER_DATA_DIR || 'user-data'),
],
watchContentBase: true,
publicPath: '/',
};
// Application pages
@ -43,6 +92,7 @@ module.exports = {
integrity,
configureWebpack,
pages,
devServer,
chainWebpack: config => {
config.module.rules.delete('svg');
},

2769
yarn.lock

File diff suppressed because it is too large Load Diff