mirror of
https://github.com/Lissy93/dashy.git
synced 2025-04-08 17:06:18 +02:00
🔀 Merge pull request #1528 from Lissy93/FEAT/Dashy-V3
[FEAT] Remove the need for rebuild after config changes
This commit is contained in:
commit
931915f366
99
.env
99
.env
@ -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
4
.github/AUTHORS.txt
vendored
@ -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
|
||||
|
1
.github/workflows/auto-tag-pr.yml
vendored
1
.github/workflows/auto-tag-pr.yml
vendored
@ -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
|
||||
|
14
.github/workflows/broadcast-message.yml
vendored
14
.github/workflows/broadcast-message.yml
vendored
@ -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 }}
|
@ -15,4 +15,3 @@ jobs:
|
||||
collapsibleThreshold: '25'
|
||||
failOnDowngrade: 'false'
|
||||
path: 'yarn.lock'
|
||||
updateComment: 'true'
|
||||
|
15
.github/workflows/generate-credits.yml
vendored
15
.github/workflows/generate-credits.yml
vendored
@ -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'
|
||||
|
6
.github/workflows/release-commenter.yml
vendored
6
.github/workflows/release-commenter.yml
vendored
@ -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
1
.yarnrc.yml
Normal file
@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
@ -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" ]
|
||||
|
71
README.md
71
README.md
@ -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&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**: [](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**: [](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 / } | / \
|
||||
(__ | / | / `.
|
||||
`- - ~ ~ -._| /_ - ~ ~ ^| /- _ `.
|
||||
| / | / ~-. ~- _
|
||||
|_____| |_____| ~ - . _ _~_-_
|
||||
-->
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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**: [](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**: [](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 |
@ -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/;
|
||||
|
@ -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.
|
||||
|
@ -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**
|
||||
|
@ -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`
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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`.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
@ -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`
|
||||
|
||||
---
|
||||
|
||||
|
@ -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).
|
||||
|
12
package.json
12
package.json
@ -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"
|
||||
|
33
server.js
33
server.js
@ -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'));
|
||||
|
@ -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
15
services/get-user.js
Normal 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 };
|
||||
}
|
||||
};
|
@ -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
|
||||
|
||||
|
@ -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}`;
|
||||
|
@ -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));
|
||||
|
@ -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);
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
@ -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');
|
||||
|
@ -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>
|
||||
|
@ -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 {};
|
||||
|
@ -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 {};
|
||||
|
@ -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 {};
|
||||
|
@ -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 {};
|
||||
|
@ -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;
|
||||
|
@ -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}`;
|
||||
|
@ -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;
|
||||
|
@ -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}`;
|
||||
|
@ -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: {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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}` : '';
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}`;
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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`;
|
||||
|
@ -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() {
|
||||
|
@ -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';
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
26
src/main.js
26
src/main.js
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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
143
src/mixins/ThemingMixin.js
Normal 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;
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
|
180
src/store.js
180
src/store.js
@ -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: {},
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 })));
|
||||
|
@ -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",
|
||||
|
@ -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
78
src/utils/HeaderAuth.js
Normal 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;
|
||||
};
|
@ -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',
|
||||
|
@ -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); },
|
||||
};
|
||||
};
|
@ -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',
|
||||
|
@ -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: {
|
||||
|
@ -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();
|
||||
|
@ -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
39
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
@ -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
|
||||
|
@ -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');
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user