🔀 Merge pull request #488 from Lissy93/FIX/general-issues

[FIX] General issues and improvements
Fixes #452
Fixes #454
Fixes #455
Fixes #463
Fixes #479
Fixes #482
Fixes #483
Fixes #485
Fixes #486
Fixes #487
This commit is contained in:
Alicia Sykes 2022-02-14 13:50:53 +00:00 committed by GitHub
commit 23f2c1af74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 920 additions and 294 deletions

12
.github/CHANGELOG.md vendored
View File

@ -1,5 +1,17 @@
# Changelog
## 🐛 2.0.3 - Bug Fixes [PR #488](https://github.com/Lissy93/dashy/pull/488)
- Press enter to submit login form (Re: #483)
- Allow disabling write to local storage and disk (Re: #485)
- Fix malformed YAML from export config (Re: #482)
- Allow global option for useProxy (Re: #486)
- Look into arrow key navigation error (Re: #463)
- Disallow displaying config (Re: #455)
- Round values in Glances Alerts widget (Re: #454)
- Create a CPU temp widget (Re: #452)
- Add to docs: Keycloak in Kubernetes (Re: #479)
- Add a widget for displaying images (Re: #487)
## ⬆️ 2.0.2 - Dependency Updates [PR #471](https://github.com/Lissy93/dashy/pull/471)
- Updates Alpine version for main Dockerfile
- Updates node_modules to latest stable versions

View File

@ -1,5 +1,4 @@
# Builds, scans and tests the multi-architecture docker image
# Then releases it to the DockerHub, GHCR and Quay registries
# Scans, builds and releases a multi-architecture docker image
name: 🐳 Build + Publish Multi-Platform Image
on:
@ -77,6 +76,9 @@ jobs:
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: 🚦 Check Registry Status
uses: crazy-max/ghaction-docker-status@v1
- name: ⚒️ Build and push
uses: docker/build-push-action@v2
with:
@ -87,11 +89,12 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
push: true
# - name: 💬 Set Docker Hub Description
# uses: peter-evans/dockerhub-description@v2
# with:
# repository: lissy93/dashy
# readme-filepath: ./README.md
# short-description: Dashy - A self-hosted start page for your server
# username: ${{ secrets.DOCKER_USERNAME }}
# password: ${{ secrets.DOCKER_PASSWORD }}
- name: 💬 Set Docker Hub Description
uses: peter-evans/dockerhub-description@v2
with:
repository: lissy93/dashy
readme-filepath: ./docker/docker-readme.md
short-description: Dashy - A self-hosted start page for your server
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@ -1,9 +1,10 @@
FROM node:16.13.2-alpine AS BUILD_IMAGE
# Set the platform to build image for
ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
# Install additional tools needed on arm64 and armv7
# Install additional tools needed if on arm64 / armv7
RUN \
case "${TARGETPLATFORM}" in \
'linux/arm64') apk add --no-cache python3 make g++ ;; \
@ -23,7 +24,7 @@ COPY . ./
# Build initial app for production
RUN yarn build
# Build the final image
# Production stage
FROM node:16.13.2-alpine
# Define some ENV Vars
@ -44,8 +45,8 @@ COPY --from=BUILD_IMAGE /app ./
ENTRYPOINT [ "/sbin/tini", "--" ]
CMD [ "yarn", "build-and-start" ]
# Expose given port
# Expose the port
EXPOSE ${PORT}
# Run simple healthchecks every 5 mins, to check the Dashy's everythings great
# Run simple healthchecks every 5 mins, to check that everythings still great
HEALTHCHECK --interval=5m --timeout=2s --start-period=30s CMD yarn health-check

136
docker/docker-readme.md Normal file
View File

@ -0,0 +1,136 @@
<h1 align="center">Dashy</h1>
<p align="center">
<i>Dashy helps you organize your self-hosted services by making them accessible from a single place</i>
<br/>
<img width="120" src="https://i.ibb.co/yhbt6CY/dashy.png" />
<br/>
<b><a href="https://github.com/Lissy93/dashy/blob/master/docs/showcase.md">User Showcase</a></b> | <b><a href="https://demo.dashy.to">Live Demo</a></b> | <b><a href="https://github.com/Lissy93/dashy/blob/master/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="https://github.com/Lissy93/dashy/blob/master/LICENSE">
<img src="https://img.shields.io/badge/License-MIT-0aa8d2?logo=opensourceinitiative&logoColor=fff" alt="License MIT">
</a>
<a href="https://github.com/Lissy93/dashy/blob/master/.github/CHANGELOG.md">
<img src="https://img.shields.io/github/package-json/v/lissy93/dashy?logo=azurepipelines&amp;color=0aa8d2" alt="Current Version">
</a>
<a href="https://hub.docker.com/r/lissy93/dashy">
<img src="https://img.shields.io/docker/pulls/lissy93/dashy?logo=docker&color=0aa8d2&logoColor=fff" alt="Docker Pulls">
</a>
<a href="http://as93.link/dashy-build-status">
<img src="https://badgen.net/github/status/lissy93/dashy?icon=github" alt="GitHub Status">
</a>
<a href="https://snyk.io/test/github/lissy93/dashy">
<img src="https://snyk.io/test/github/lissy93/dashy/badge.svg" alt="Known Vulnerabilities">
</a>
</p>
## Features 🌈
- 🔎 Instant search by name, domain, or tags + customizable hotkeys & keyboard shortcuts
- 🎨 Multiple built-in color themes, with UI color editor and support for custom CSS
- 🧸 Many icon options - Font-Awesome, homelab icons, auto-fetching Favicon, images, emojis, etc.
- 🚦 Status monitoring for each of your apps/links for basic availability and uptime checking
- 📊 Widgets for displaying info and dynamic content from your self-hosted services
- 💂 Optional authentication with multi-user access, configurable privileges, and SSO support
- 🌎 Multi-language support, with 10+ human-translated languages, and more on the way
- ☁ Optional, encrypted, free off-site cloud backup and restore feature available
- 💼 A workspace view, for easily switching between multiple apps simultaneously
- 🛩️ A minimal view, for use as a fast-loading browser Startpage
- 🖱️ Choose app launch method, either new tab, same tab, a pop-up modal, or in the workspace view
- 📏 Customizable layout, sizes, text, component visibility, sort order, behavior, etc.
- 🖼️ Options for a full-screen background image, custom nav-bar links, HTML footer, title, etc.
- 🚀 Easy to setup with Docker, or on bare metal, or with 1-Click cloud deployment
- ⚙️ Easy configuration, either through the UI, or using a YAML file
- ✨ Under active development with improvements and new features added regularly
- 🤏 Small bundle size, fully responsive UI, and PWA for basic offline access
- 🆓 100% free and open-source
- 🔐 Strong focus on privacy
- 🌈 And loads more...
## Demo ⚡
**Live Instances**: [Demo 1](https://demo.dashy.to) (Live Demo) ┆ [Demo 2](https://live.dashy.to) (Dashy Links) ┆ [Demo 3](https://dev.dashy.to) (Dev Preview)
**Screenshots**: Checkout the [Showcase](https://github.com/Lissy93/dashy/blob/master/docs/showcase.md), to see example dashboards from the community
**Spin up your own demo**: [![One-Click Deploy with PWD](https://img.shields.io/badge/Play--with--Docker-Deploy-2496ed?style=flat-square&logo=docker)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml) or [`docker run -p 8080:80 lissy93/dashy`](./docs/quick-start.md)
<p align="center">
<img width="800" src="https://i.ibb.co/L8YbNNc/dashy-demo2.gif" alt="Demo" />
</p>
**[⬆️ Back to Top](#dashy)**
---
## Getting Started 🛫
To deploy Dashy with Docker, just run `docker run -p 8080:80 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).
Dashy can also be run on bare metal using Node.js, or deployed to a cloud service, using the 1-Click deploy script.
---
## Documentation 📝
#### Running Dashy
- **[Quick Start](https://github.com/Lissy93/dashy/blob/master/docs/quick-start.md)** - TDLR guide on getting Dashy up and running
- **[Deployment](https://github.com/Lissy93/dashy/blob/master/docs/deployment.md)** - Full guide on deploying Dashy either locally or online
- **[Configuring](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md)** - Complete list of all available options in the config file
- **[App Management](https://github.com/Lissy93/dashy/blob/master/docs/management.md)** - Managing your app, updating, security, web server configuration, etc
- **[Troubleshooting](https://github.com/Lissy93/dashy/blob/master/docs/troubleshooting.md)** - Common errors and problems, and how to fix them
#### Feature Docs
- **[Authentication](https://github.com/Lissy93/dashy/blob/master/docs/authentication.md)** - Guide to setting up authentication to protect your dashboard
- **[Alternate Views](https://github.com/Lissy93/dashy/blob/master/docs/alternate-views.md)** - Outline of available pages / views and item opening methods
- **[Backup & Restore](https://github.com/Lissy93/dashy/blob/master/docs/backup-restore.md)** - Guide to backing up config with Dashy's cloud sync feature
- **[Icons](https://github.com/Lissy93/dashy/blob/master/docs/icons.md)** - Outline of all available icon types for sections and items, with examples
- **[Language Switching](https://github.com/Lissy93/dashy/blob/master/docs/multi-language-support.md)** - Details on how to switch language, or add a new locale
- **[Status Indicators](https://github.com/Lissy93/dashy/blob/master/docs/status-indicators.md)** - Using Dashy to monitor uptime and status of your apps
- **[Searching & Shortcuts](https://github.com/Lissy93/dashy/blob/master/docs/searching.md)** - Searching, launching methods + keyboard shortcuts
- **[Theming](https://github.com/Lissy93/dashy/blob/master/docs/theming.md)** - Complete guide to applying, writing and modifying themes + styles
- **[Widgets](https://github.com/Lissy93/dashy/blob/master/docs/widgets.md)** - List of all dynamic content widgets, with usage guides and examples
#### Development and Contributing
- **[Developing](https://github.com/Lissy93/dashy/blob/master/docs/developing.md)** - Running Dashy development server locally, and general workflow
- **[Development Guides](https://github.com/Lissy93/dashy/blob/master/docs/development-guides.md)** - Common development tasks, to help new contributors
- **[Contributing](https://github.com/Lissy93/dashy/blob/master/docs/contributing.md)** - How you can help keep Dashy alive
- **[Showcase](https://github.com/Lissy93/dashy/blob/master/docs/showcase.md)** - See how others are using Dashy, and share your dashboard
- **[Credits](https://github.com/Lissy93/dashy/blob/master/docs/credits.md)** - List of people and projects that have made Dashy possible
- **[Release Workflow](https://github.com/Lissy93/dashy/blob/master/docs/release-workflow.md)** - Info about releases, CI and automated tasks
---
## License 📜
Dashy is Licensed under [MIT X11](https://en.wikipedia.org/wiki/MIT_License)
```
Copyright © 2021 Alicia Sykes <https://aliciasykes.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
Except as contained in this notice, Dashy shall not be used in advertising or otherwise
to promote the sale, use, or other dealings in this Software without prior written
authorization from the repo owner.
```

View File

@ -74,13 +74,15 @@ For Example:
...
```
### Security
Since all authentication is happening entirely on the client-side, it is vulnerable to manipulation by an adversary. An attacker could look at the source code, find the function used generate the auth token, then decode the minified JavaScript to find the hash, and manually generate a token using it, then just insert that value as a cookie using the console, and become a logged in user. Therefore, if you need secure authentication for your app, it is strongly recommended to implement this using your web server, or use a VPN to control access to Dashy. The purpose of the login page is merely to prevent immediate unauthorized access to your homepage.
### Permissions
Any user who is not an admin (with `type: admin`) will not be able to write changes to disk.
Addressing this is on the todo list, and there are several potential solutions:
1. Encrypt all site data against the users password, so that an attacker can not physically access any data without the correct decryption key
2. Use a backend service to handle authentication and configuration, with no user data returned from the server until the correct credentials are provided. However, this would require either Dashy to be run using it's Node.js server, or the use of an external service
3. ~~Implement authentication using a self-hosted identity management solution, such as [Keycloak for Vue](https://www.keycloak.org/securing-apps/vue)~~ **This is now implemented, and released in PR #174 of V 1.6.5!**
You can also prevent any user from writing changes to disk, using `preventWriteToDisk`. Or prevent any changes from being saved locally in browser storage, using `preventLocalSave`. Both properties can be found under [`appConfig`](./docs/configuring.md#appconfig-optional).
To disable all UI config features, including View Config, set `disableConfiguration`.
### Security
With basic auth, all logic is happening on the client-side, which could mean a skilled user could manipulate the code to view parts of your configuration, including the hash. If the SHA-256 hash is of a common password, it may be possible to determine it, using a lookup table, in order to find the original password. Which can be used to manually generate the auth token, that can then be inserted into session storage, to become a valid logged in user. Therefore, you should always use a long, strong and unique password, and if you instance contains security-critical info and/ or is exposed directly to the internet, and alternative authentication method may be better. The purpose of the login page is merely to prevent immediate unauthorized access to your homepage.
**[⬆️ Back to Top](#authentication)**

View File

@ -1,45 +1,54 @@
# 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. Changes can also be made [directly through the UI](#editing-config-through-the-ui) and previewed live, from here you can also export, backup, reset, validate and download your configuration file.
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.
#### There are three ways to edit the config
- **Directly in the YAML file** _(5/5 reliability, 3/5 usability)_
- Write changes directly to the conf.yml file, optionally using one of the templates provided. This can be done in your favorite editor and uploading to your server, or directly editing the file via SSH, but the easiest method would be to use [Code Server](https://github.com/coder/code-server)
- **UI JSON Editor** _(4/5 reliability, 4/5 usability)_
- From the UI, under the config menu there is a JSON editor, with built-in validation, documentation and advanced options
- **UI Visual Editor** _(3/5 reliability, 5/5 usability)_
- From the UI, enter the Interactive Edit Mode, then click any part of the page to edit. Changes are previewed live, and then saved to disk
- **REST API** _(Coming soon)_
- Programmatically edit config either through the command line, using a script or a third-party application
#### Tips
- You may find it helpful to look at some sample config files to get you started, a collection of which can be found [here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10)
- You can check that your config file fits the schema, by running `yarn validate-config`
- After modifying your config, the app needs to be recompiled, by running `yarn build` - this happens automatically if you're using Docker
- It is recommended to keep a backup of your config file. You can download it under Config menu, or use the [Cloud Backup](./docs/backup-restore.md) feature.
- You can make use of YAML features, like anchors, comments, multi-line strings, etc to reuse attributes and keep your config file readable
- Once you have finished configuring your dashboard, you can choose to [disable UI config](#preventing-changes) if you wish
- All fields are optional, unless otherwise stated.
The following file provides a reference of all supported configuration options.
---
#### Contents
- [**`pageInfo`**](#pageinfo) - Header text, footer, title, navigation, etc
- [`navLinks`](#pageinfonavlinks-optional) - Navigation bar items and links
- [`navLinks`](#pageinfonavlinks-optional) - Links to display in the navigation bar
- [**`appConfig`**](#appconfig-optional) - Main application settings
- [`webSearch`](#appconfigwebsearch-optional) - Configure web search engine options
- [`hideComponents`](#appconfighidecomponents-optional) - Show/ hide page components
- [`auth`](#appconfigauth-optional) - Built-in authentication setup
- [`users`](#appconfigauthusers-optional) - Setup for simple auth
- [`keycloak`](#appconfigauthkeycloak-optional) - Auth using Keycloak
- [`webSearch`](#appconfigwebsearch-optional) - Configure web search engine options
- [`hideComponents`](#appconfighidecomponents-optional) - Show/ hide page components
- [`auth`](#appconfigauth-optional) - Built-in authentication setup
- [`users`](#appconfigauthusers-optional) - List or users (for simple auth)
- [`keycloak`](#appconfigauthkeycloak-optional) - Auth config for Keycloak
- [**`sections`**](#section) - List of sections
- [`displayData`](#sectiondisplaydata-optional) - Section display settings
- [`icon`](#sectionicon-and-sectionitemicon) - Icon for a section
- [`items`](#sectionitem) - List of items
- [`icon`](#sectionicon-and-sectionitemicon) - Icon for an item
- [`displayData`](#sectiondisplaydata-optional) - Section display settings
- [`show/hideForKeycloakUsers`](#sectiondisplaydatahideforkeycloakusers-and-sectiondisplaydatashowforkeycloakusers) - Set user controls
- [`icon`](#sectionicon-and-sectionitemicon) - Icon for a section
- [`items`](#sectionitem) - List of items
- [`icon`](#sectionicon-and-sectionitemicon) - Icon for an item
- [`widgets`](#sectionwidget-optional) - List of widgets
- [**Notes**](#notes)
- [Editing Config through the UI](#editing-config-through-the-ui)
- [About YAML](#about-yaml)
- [Config Saving Methods](#config-saving-methods)
- [Preventing Changes](#preventing-changes-being-written-to-disk)
- [About YAML](#about-yaml)
- [Config Saving Methods](#config-saving-methods)
- [Preventing Changes](#preventing-changes)
- [Example](#example)
---
Tips:
- You may find it helpful to look at some sample config files to get you started, a collection of which can be found [here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10)
- You can check that your config file fits the schema, by running `yarn validate-config`
- After modifying your config, the app needs to be recompiled, by running `yarn build` - this happens automatically whilst the app is running if you're using Docker
- It is recommended to make and keep a backup of your config file. You can download your current config through the UI either from the Config menu, or using the `/download` endpoint. Alternatively, you can use the [Cloud Backup](./docs/backup-restore.md) feature.
- The config can also be modified directly through the UI, validated and written to the conf.yml file.
- All fields are optional, unless otherwise stated.
---
### Top-Level Fields
**Field** | **Type** | **Required**| **Description**
@ -98,7 +107,10 @@ Tips:
**`routingMode`** | `string` | _Optional_ | Can be either `hash` or `history`. Determines the URL format for sub-pages, hash mode will look like `/#/home` whereas with history mode available you have nice clean URLs, like `/home`. For more info, see the [Vue docs](https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations). If you're hosting Dashy with a custom BASE_URL, you will find that a bit of extra server config is necessary to get history mode working, so here you may want to instead use `hash` mode.Defaults to `history`.
**`enableMultiTasking`** | `boolean` | _Optional_ | If set to true, will keep apps open in the background when in the workspace view. Useful for quickly switching between multiple sites, and preserving their state, but comes at the cost of performance.
**`workspaceLandingUrl`** | `string` | _Optional_ | The URL or an app, service or website to launch when the workspace view is opened, before another service has been launched
**`allowConfigEdit`** | `boolean` | _Optional_ | Should prevent / allow the user to write configuration changes to the conf.yml from the UI. When set to `false`, the user can only apply changes locally using the config editor within the app, whereas if set to `true` then changes can be written to disk directly through the UI. Defaults to `true`. Note that if authentication is enabled, the user must be of type `admin` in order to apply changes globally.
**`preventWriteToDisk`** | `boolean` | _Optional_ | If set to `true`, users will be prevented from saving config changes to disk through the UI
**`preventLocalSave`** | `boolean` | _Optional_ | If set to `true`, users will be prevented from applying config changes to local storage
**`disableConfiguration`** | `boolean` | _Optional_ | If set to true, no users will be able to view or edit the config through the UI
**`widgetsAlwaysUseProxy`** | `boolean` | _Optional_ | If set to `true`, requests made by widgets will always be proxied, same as setting `useProxy: true` on each widget. Note that this may break some widgets.
**`showSplashScreen`** | `boolean` | _Optional_ | If set to `true`, a loading screen will be shown. Defaults to `false`.
**`enableErrorReporting`** | `boolean` | _Optional_ | Enable reporting of unexpected errors and crashes. This is off by default, and **no data will ever be captured unless you explicitly enable it**. Turning on error reporting helps previously unknown bugs get discovered and fixed. Dashy uses [Sentry](https://github.com/getsentry/sentry) for error reporting. Defaults to `false`.
**`sentryDsn`** | `boolean` | _Optional_ | If you need to monitor errors in your instance, then you can use Sentry to collect and process bug reports. Sentry can be self-hosted, or used as SaaS, once your instance is setup, then all you need to do is pass in the DSN here, and enable error reporting. You can learn more on the [Sentry DSN Docs](https://docs.sentry.io/product/sentry-basics/dsn-explainer/). Note that this will only ever be used if `enableErrorReporting` is explicitly enabled.
@ -280,8 +292,13 @@ When updating the config through the JSON editor in the UI, you have two save op
- Changes saved locally will only be applied to the current user through the browser, and will not apply to other instances - you either need to use the cloud sync feature, or manually update the conf.yml file.
- On the other-hand, if you choose to write changes to disk, then your main `conf.yml` file will be updated, and changes will be applied to all users, and visible across all devices. For this functionality to work, you must be running Dashy with using the Docker container, or the Node server. A backup of your current configuration will also be saved in the same directory.
### Preventing Changes being Written to Disk
To disallow any changes from being written to disk via the UI config editor, set `appConfig.allowConfigEdit: false`. If you are using users, and have setup `auth` within Dashy, then only users with `type: admin` will be able to write config changes to disk.
### Preventing Changes
If you have authentication set up, then any user who is not an admin (with `type: admin`) will not be able to write changes to disk.
You can also prevent changes fro any user being written to disk, using `preventWriteToDisk`. Or prevent any changes from being saved locally in browser storage, using `preventLocalSave`.
To disable all UI config features, set `disableConfiguration`.
### Example

View File

@ -11,6 +11,7 @@ Sections:
- [Hiding Page Furniture](#hiding-page-furniture-on-certain-routes)
- [Adding / Using Environmental Variables](#adding--using-environmental-variables)
- [Building a Widget](#building-a-widget)
- [Respecting Config Permissions](#respecting-config-permissions)
## Creating a new theme
@ -94,18 +95,34 @@ If you are not comfortable with making pull requests, or do not want to modify t
# Adding a new option in the config file
This section is for, if you're adding a new component or setting, that requires an additional item to be added to the users config file.
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.
Before adding a new option in the config file, first ensure that there is nothing similar available, that is is definitely necessary, it will not conflict with any other options and most importantly that it will not cause any breaking changes. Ensure that you choose an appropriate and relevant section to place it under.
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 decide the most appropriate place for your attribute:
Next choose the appropriate section to place it under
- Application settings should be located under `appConfig`
- Page info (such as text and metadata) should be under `pageInfo`
- Data relating to specific sections should be under `section[n].displayData`
- And for setting applied to specific items, it should be under `item[n]`
- Settings applied to specific items or widgets, should be under `item[n]` or `widget[n]`
In order for the user to be able to add your new attribute using the Config Editor, and for the build validation to pass, your attribute must be included within the [ConfigSchema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js). You can read about how to do this on the [ajv docs](https://ajv.js.org/json-schema.html). Give your property a type and a description, as well as any other optional fields that you feel are relevant. For example:
For example, if your option is added under `appConfig`, you can access it within your component using the `$store`, this is typically placed in a computed property, e.g:
```javascript
computed: {
appConfig() {
return this.$store.getters.appConfig;
},
...
},
```
Then, where you want get the users value within your component, use something like: `this.appConfig.myProperty`. Don't forget to have a fallback or default for then the user hasn't specified it.
If you have a default fallback value, then this would typically be specified in the [`defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js) file.
You will now need to add the definition of your new attribute into the [ConfigSchema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js). This will make it available in the UI config editor, and also ensure that the config validation check doesn't fail.
For example:
```json
"fontAwesomeKey": {
@ -124,24 +141,21 @@ or
}
```
Next, if you're property should have a default value, then add it to [`defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js). This ensures that nothing will break if the user does not use your property, and having all defaults together keeps things organised and easy to manage.
If your property needs additional logic for fetching, setting or processing, then you can add a helper function within [`ConfigHelpers.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigHelpers.js).
Finally, add your new property to the [`configuring.md`](./configuring.md) API docs. Put it under the relevant section, and be sure to include field name, data type, a description and mention that it is optional. If your new feature needs more explaining, then you can also document it under the relevant section elsewhere in the documentation.
Checklist:
- [ ] Ensure the new attribute is actually necessary, and nothing similar already exists
- [ ] Update the [Schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js) with the parameters for your new option
- [ ] Set a default value (if required) within [`defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js)
- [ ] Document the new value in [`configuring.md`](./configuring.md)
- [ ] Test that the reading of the new attribute is properly handled, and will not cause any errors when it is missing or populated with an unexpected value
- [ ] If required, set a default or fallback value (usually in [`defaults.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/defaults.js))
- [ ] Document the new value in [`configuring.md`](./configuring.md), and if required under the relevant section in the docs
- [ ] Ensure your changes are backwards compatible, and that nothing breaks if the attribute isn't specified
---
## Updating Dependencies
Running `yarn upgrade` will updated all dependencies based on the ranges specified in the `package.json`. The `yarn.lock` file will be updated, as will the contents of `./node_modules`, for more info, see the [yarn upgrade documentation](https://classic.yarnpkg.com/en/docs/cli/upgrade/). It is important to thoroughly test after any big dependency updates.
Running `yarn upgrade` will updated all dependencies based on the ranges specified in the `package.json`. The `yarn.lock` file will be updated, as will the contents of `./node_modules`, for more info, see the [yarn upgrade documentation](https://classic.yarnpkg.com/en/docs/cli/upgrade/). [`npm-check-updates`](https://github.com/raineorshine/npm-check-updates) is a useful tool to help with this.
It is important to thoroughly test after any big dependency updates.
---
@ -430,3 +444,31 @@ Finally, add some documentation for your widget in the [Widget Docs](https://git
**Summary**: For a complete example of everything discussed here, see: [`3da76ce`](https://github.com/Lissy93/dashy/commit/3da76ce2999f57f76a97454c0276301e39957b8e)
---
## Respecting Config Permissions
Any screen that displays part or all of the users config, must not be shown when the user has disabled viewing config.
This can be done by checking the `allowViewConfig` attribute of the `permissions` getter, in the store.
First create a new `computed` property, like:
```
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
```
Then wrap the part of your UI which displays config with: `v-if="allowViewConfig"`
If required, add a message showing that the component isn't available, using the `AccessError` component. E.g.
```
import AccessError from '@/components/Configuration/AccessError';
```
```
<AccessError v-else />
```
The `$store.getters.permissions` object also returns options for when and where config can be saved, using: `allowWriteToDisk`, and `allowSaveLocally` - both are booleans.

View File

@ -22,6 +22,7 @@
- [Status Checks Failing](#status-checks-failing)
- [Diagnosing Widget Errors](#widget-errors)
- [Fixing Widget CORS Errors](#widget-cors-errors)
- [Keycloak Redirect Error](#keycloak-redirect-error)
- [How-To Open Browser Console](#how-to-open-browser-console)
- [Git Contributions not Displaying](#git-contributions-not-displaying)
@ -273,6 +274,25 @@ For testing purposes, you can use an addon, which will disable the CORS checks.
---
## Keycloak Redirect Error
Firstly, ensure that in your Keycloak instance you have populated the Valid Redirect URIs field ([screenshot](https://user-images.githubusercontent.com/1862727/148599768-db4ee4f8-72c5-402d-8f00-051d999e6267.png)) with the URL to your Dashy instance.
You may need to specify CORS headers on your Keycloak instance, to allow requests coming from Dashy, e.g:
```
Access-Control-Allow-Origin: https://dashy.example.com
```
If you're running in Kubernetes, you will need to enable CORS ingress rules, see [docs](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#enable-cors), e.g:
```
nginx.ingress.kubernetes.io/cors-allow-origin: "https://dashy.example.com"
nginx.ingress.kubernetes.io/enable-cors: "true"
```
---
## How-To Open Browser Console
When raising a bug, one crucial piece of info needed is the browser's console output. This will help the developer diagnose and fix the issue.

View File

@ -12,6 +12,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [Weather](#weather)
- [Weather Forecast](#weather-forecast)
- [RSS Feed](#rss-feed)
- [Image](#image)
- [Public IP Address](#public-ip)
- [Crypto Watch List](#crypto-watch-list)
- [Crypto Price History](#crypto-token-price-history)
@ -57,6 +58,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [Network Traffic](#network-traffic)
- [Resource Usage Alerts](#resource-usage-alerts)
- [Public & Private IP](#ip-address)
- [CPU Temperature](#cpu-temp)
- **[Dynamic Widgets](#dynamic-widgets)**
- [Iframe Widget](#iframe-widget)
- [HTML Embed Widget](#html-embedded-widget)
@ -209,6 +211,41 @@ Display news and updates from any RSS-enabled service.
---
### Image
Displays an image.
This may be useful if you have a service (such as Grafana - [see example](https://mattionline.de/grafana-api-export-graph-as-png/)), which periodically exports charts or other data as an image.
You can also store images within Dashy's public directory (using a Docker volume), and reference them directly. E.g. `-v ./path/to/my-homelab-logo.png:/app/public/logo.png`, then in the widget `imagePath: /logo.png`.
Similarly, any web service that serves up widgets as image can be used. E.g. you could show current star chart for a GitHub repo, with: `imagePath: https://starchart.cc/Lissy93/dashy.svg`.
If you'd like to embed a live screenshot, of all or just part of a website, then this can be done using [API Flash](https://apiflash.com/).
Or what about showing a photo of the day? Try `https://source.unsplash.com/random/400x300` or `https://picsum.photos/400/300`
<p align="center"><img width="300" src="https://i.ibb.co/P48Y443/image-widget.png" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`imagePath`** | `string` | Required | The path (local or remote) of the image to display
##### Example
```yaml
- type: image
options:
imagePath: https://i.ibb.co/yhbt6CY/dashy.png
```
##### Info
Unless image fetched from remote source, no external data request is made.
---
### Public IP
Often find yourself searching "What's my IP", just so you can check your VPN is still connected? This widget displays your public IP address, along with ISP name and approx location. Data can be fetched from either [IpApi.co](https://ipapi.co/), [IP-API.com](https://ip-api.com/) or [IpGeolocation.io](https://ipgeolocation.io/).
@ -1488,6 +1525,25 @@ Shows public and private IP address. Note that the ip plugin is not available on
---
### CPU Temp
Displays temperature data from system CPUs.
Note: This widget uses the [`sensors`](https://github.com/nicolargo/glances/blob/develop/glances/plugins/glances_sensors.py) plugin, which is disabled by default, and may cause [performance issues](https://github.com/nicolargo/glances/issues/1664#issuecomment-632063558).
You'll need to enable the sensors plugin to use this widget, using: `--enable-plugin sensors` when you start Glances.
<p align="center"><img width="400" src="https://i.ibb.co/xSs4Gqd/gl-cpu-temp.png" /></p>
##### Example
```yaml
- type: gl-cpu-temp
options:
hostname: http://192.168.130.2:61208
```
---
## Dynamic Widgets
### Iframe Widget
@ -1556,6 +1612,14 @@ Or
scriptSrc: 'https://files.coinmarketcap.com/static/widget/currency.js'
```
You can also use this widget to display an image, wither locally or from a remote origin.
```yaml
- type: embed
options:
html: '<img src="https://dashy.lan/item-icons/my-image.png" />'
```
---
### API Response
@ -1786,4 +1850,4 @@ For testing purposes, you can use an addon, which will disable the CORS checks.
### Raising an Issue
If you need to submit a bug report for a failing widget, then please include the full console output (see [how](/docs/troubleshooting.md#how-to-open-browser-console)) as well as the relevant parts of your config file. Before sending the request, ensure you've read the docs. If you're new to GitHub, an haven't previously contributed to the project, then please fist star the repo to avoid your ticket being closed by the anti-spam bot.
If you need to submit a bug report for a failing widget, then please include the full console output (see [how](/docs/troubleshooting.md#how-to-open-browser-console)) as well as the relevant parts of your config file. Before sending the request, ensure you've read the docs. If you're new to GitHub, an haven't previously contributed to the project, then please fist star the repo to avoid your ticket being closed by the anti-spam bot.

View File

@ -1,6 +1,6 @@
{
"name": "Dashy",
"version": "2.0.2",
"version": "2.0.3",
"license": "MIT",
"main": "server",
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
@ -64,9 +64,6 @@
"vue-svg-loader": "^0.16.0",
"vue-template-compiler": "^2.6.14"
},
"gitHooks": {
"pre-commit": "yarn lint"
},
"engines": {
"node": ">=16.0.0"
},

View File

@ -1,8 +1,11 @@
/* eslint-disable no-console */
/* Script that validates the conf.yml file against Dashy's schema, and outputs any issues */
const Ajv = require('ajv');
const yaml = require('js-yaml');
const fs = require('fs');
/**
* Checks that conf.yml is present + parsable, then validates it against the schema
* Prints detailed info about any errors or warnings to help the user fix the issue
*/
const fs = require('fs'); // For opening + reading files
const yaml = require('js-yaml'); // For parsing YAML
const Ajv = require('ajv'); // For validating with schema
const schema = require('../src/utils/ConfigSchema.json');
@ -17,32 +20,29 @@ const validatorOptions = {
const ajv = new Ajv(validatorOptions);
/* Message printed when validation was successful */
const successMsg = () => '\x1b[1m\x1b[32mNo issues found, your configuration is valid :)\x1b[0m\n';
const successMsg = () => '\x1b[1m\x1b[32m✔️ Config file is valid, no issues found\x1b[0m\n';
/* Just a wrapper to system's console.log */
const logToConsole = (msg) => { console.log(msg); };
const logToConsole = (msg) => { console.log(msg || '\n'); }; // eslint-disable-line no-console
/* Formats error message. ready for printing to the console */
const errorMsg = (output) => {
const warningFont = '\x1b[103m\x1b[34m';
const line = `${warningFont}${new Array(42).fill('━').join('')}\x1b[0m`;
const formatParams = (params) => {
if (params.additionalProperty) return `(${params.additionalProperty})`;
return '';
};
let msg = `\n${line}\n${warningFont} Warning: ${output.length} `
+ `issue${output.length > 1 ? 's' : ''} found in config file \x1b[0m\n${line}\n`;
output.forEach((details, index) => {
msg += `${'\x1b[36m'}${index + 1}. ${details.keyword} ${details.message} `
+ `in ${details.instancePath}\x1b[0m\n`;
msg += `${'\x1b[36m'}${index + 1}. \x1b[4m${details.instancePath}\x1b[0m\x1b[36m `
+ `${details.message} ${formatParams(details.params)}\x1b[0m\n`;
});
return msg;
};
/* Error message printed when the file could not be opened */
const bigError = () => {
const formatting = '\x1b[30m\x1b[43m';
const line = `${formatting}${new Array(38).fill('━').join('')}\x1b[0m\n`;
const msg = `${formatting} Error, unable to validate 'conf.yml' \x1b[0m\n`;
return `\n${line}${msg}${line}\n`;
};
/* Sets valid status as environmental variable */
const setIsValidVariable = (isValid) => {
process.env.VUE_APP_CONFIG_VALID = isValid;
};
@ -60,16 +60,49 @@ const validate = (config) => {
}
};
try {
/* Error message printed when the file could not be opened */
const bigError = () => {
const formatting = '\x1b[30m\x1b[43m';
const line = `${formatting}${new Array(38).fill('━').join('')}\x1b[0m\n`;
const msg = `${formatting} Error, unable to validate 'conf.yml' \x1b[0m\n`;
return `\n${line}${msg}${line}`;
};
/* Given an error object, prints helpful info to the user */
const printFileReadError = (e) => {
let customError = '';
if (e.mark) { // YAML syntax error
customError = `\x1b[33m\x1b[4m⚠ Error on line ${e.mark.line}, column ${e.mark.column}: `
+ `${e.reason}\x1b[0m\n\n${e.mark.snippet}\n\x1b[0m`
+ '\n\x1b[36m You might find it helpful to use a YAML validator'
+ ', like: \x1b[4mhttps://yamlchecker.com/\x1b[0m\n';
}
if (e.code === 'ENOENT') { // File not found error
customError = `\x1b[33m⚠ Config file could not be found at ${e.path}\x1b[0m\n`;
}
if (e.code === 'EISDIR') { // Not a file
customError = '\x1b[33m⚠ Config needs to be a file, but found a directory instead \x1b[0m\n';
}
if (e.code === 'EACCES' || e.code === 'EPERM') { // File permissions error
customError = '\x1b[33m⚠ Permission denied \x1b[0m\n';
}
logToConsole(customError);
if (customError === '') { // Unknown error, print stack trace
const moreInfo = 'Ensure that your config file is present, readable, and valid YAML. '
+ 'If this issue persists, you can get support by raising a ticket on GitHub. '
+ 'Please include the following stack trace';
logToConsole(moreInfo);
// eslint-disable-next-line no-console
console.warn('\x1b[33mStack Trace for config-validator.js:\x1b[0m\n', e);
logToConsole();
}
};
try { // Try to open and parse the YAML file
const config = yaml.load(fs.readFileSync('./public/conf.yml', 'utf8'));
validate(config);
} catch (e) { // Something went very wrong...
setIsValidVariable(false);
logToConsole(bigError());
logToConsole('Please ensure that your config file is present, '
+ 'has the correct access rights and is parsable. '
+ 'If this warning persists, it may be an issue with the '
+ 'validator function. Please raise an issue, and include the following stack trace:\n');
console.warn('\x1b[33mStack Trace for config-validator.js:\x1b[0m\n', e);
logToConsole('\n\n');
printFileReadError(e);
}

View File

@ -0,0 +1,37 @@
<template>
<div class="error">
<p>
Error: Configuration has been disabled on this instance.
Please contact your administrator for more information.
</p>
</div>
</template>
<script>
import ErrorHandler from '@/utils/ErrorHandler';
export default {
name: 'AccessError',
mounted() {
ErrorHandler('Access Error: Config has been disabled on this instance');
},
};
</script>
<style scoped lang="scss">
.error {
padding: 0.5rem 1rem;
min-width: 20rem;
width: 50%;
margin: 2rem auto;
cursor: default;
text-align: center;
font-weight: bold;
font-size: 1.2rem;
color: var(--warning);
border-radius: var(--curve-factor);
border: 1px dashed var(--warning);
background: var(--background);
}
</style>

View File

@ -30,7 +30,8 @@
<a href="https://github.com/Lissy93/dashy/blob/master/.github/SECURITY.md">Security Policy</a>
<!-- License -->
<h3>License</h3>
Licensed under MIT X11. Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © 2021.<br>
Licensed under <a href="https://github.com/Lissy93/dashy/blob/master/LICENSE">MIT X11</a>.
Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © 2021.<br>
For licenses for third-party modules, please see <a href="https://github.com/Lissy93/dashy/blob/master/.github/LEGAL.md">Legal</a>.<br>
For the full list of contributors and thanks, see <a href="https://github.com/Lissy93/dashy/blob/master/docs/credits.md">Credits</a>.
<!-- App Version -->

View File

@ -1,49 +1,61 @@
<template>
<Tabs :navAuto="true" name="Add Item" ref="tabView">
<Tabs :navAuto="true" name="Add Item" ref="tabView" v-bind:class="{ hideTabs: !enableConfig }">
<!-- Main tab -->
<TabItem :name="$t('config.main-tab')" class="main-tab">
<div class="main-options-container">
<div class="config-buttons">
<h2>{{ $t('config.heading') }}</h2>
<a class="hyperlink-wrapper" @click="openExportConfigModal()">
<button class="config-button center">
<DownloadIcon class="button-icon"/>
{{ $t('config.download-config-button') }}
</button>
</a>
<button class="config-button center" @click="() => navigateToTab(1)">
<EditIcon class="button-icon"/>
<!-- Export config button -->
<Button class="config-button" :disallow="!enableConfig" :click="openExportConfigModal">
{{ $t('config.download-config-button') }}
<DownloadIcon class="button-icon"/>
</Button>
<!-- Edit config button -->
<Button class="config-button" :disallow="!enableConfig" :click="openEditConfigTab">
{{ $t('config.edit-config-button') }}
</button>
<button class="config-button center" @click="openLanguageSwitchModal()">
<LanguageIcon class="button-icon"/>
<EditIcon class="button-icon"/>
</Button>
<!-- Language switcher button -->
<Button class="config-button" :click="openLanguageSwitchModal">
{{ $t('config.change-language-button') }}
</button>
<button class="config-button center" @click="() => navigateToTab(3)">
<CustomCssIcon class="button-icon"/>
<LanguageIcon class="button-icon"/>
</Button>
<!-- CSS / Styling button -->
<Button class="config-button" :disallow="!enableConfig" :click="openEditCssTab">
{{ $t('config.edit-css-button') }}
</button>
<button class="config-button center" @click="() => navigateToTab(2)">
<CloudIcon class="button-icon"/>
<CustomCssIcon class="button-icon"/>
</Button>
<!-- Cloud sync button -->
<Button class="config-button" :disallow="!enableConfig" :click="openCloudSyncTab">
{{backupId ? $t('config.edit-cloud-sync-button') : $t('config.cloud-sync-button') }}
</button>
<button class="config-button center" @click="openRebuildAppModal()">
<RebuildIcon class="button-icon"/>
<CloudIcon class="button-icon"/>
</Button>
<!-- Rebuild app button -->
<Button class="config-button" :disallow="!enableConfig" :click="openRebuildAppModal">
{{ $t('config.rebuild-app-button') }}
</button>
<button class="config-button center" @click="resetLocalSettings()">
<DeleteIcon class="button-icon"/>
<RebuildIcon class="button-icon"/>
</Button>
<!-- Reset local changes button -->
<Button class="config-button" :click="resetLocalSettings">
{{ $t('config.reset-settings-button') }}
</button>
<button class="config-button center" @click="openAboutModal()">
<IconAbout class="button-icon" />
<DeleteIcon class="button-icon"/>
</Button>
<!-- About modal button -->
<Button class="config-button" :click="openAboutModal">
{{ $t('config.app-info-button') }}
</button>
<p class="small-screen-note" style="display: none;">
You are using a very small screen, and some screens in this menu may not be optimal
</p>
<IconAbout class="button-icon" />
</Button>
<!-- Display app version and language -->
<p class="language">{{ getLanguage() }}</p>
<AppVersion />
</div>
<!-- Display note if Config disabled, or if on mobile -->
<p v-if="!enableConfig" class="config-disabled-note">
Some configuration features have been disabled by your administrator
</p>
<p class="small-screen-note" style="display: none;">
You are using a very small screen, and some screens in this menu may not be optimal
</p>
<div class="config-note">
<span>{{ $t('config.backup-note') }}</span>
</div>
@ -51,13 +63,13 @@
<!-- Rebuild App Modal -->
<RebuildApp />
</TabItem>
<TabItem :name="$t('config.edit-config-tab')">
<TabItem :name="$t('config.edit-config-tab')" v-if="enableConfig">
<JsonEditor />
</TabItem>
<TabItem :name="$t('cloud-sync.title')">
<TabItem :name="$t('cloud-sync.title')" v-if="enableConfig">
<CloudBackupRestore />
</TabItem>
<TabItem :name="$t('config.custom-css-tab')">
<TabItem :name="$t('config.custom-css-tab')" v-if="enableConfig">
<CustomCssEditor />
</TabItem>
</Tabs>
@ -65,15 +77,16 @@
<script>
import JsonToYaml from '@/utils/JsonToYaml';
import { localStorageKeys, modalNames } from '@/utils/defaults';
import { getUsersLanguage } from '@/utils/ConfigHelpers';
import ErrorHandler from '@/utils/ErrorHandler';
import StoreKeys from '@/utils/StoreMutations';
import JsonEditor from '@/components/Configuration/JsonEditor';
import CustomCssEditor from '@/components/Configuration/CustomCss';
import CloudBackupRestore from '@/components/Configuration/CloudBackupRestore';
import RebuildApp from '@/components/Configuration/RebuildApp';
import AppVersion from '@/components/Configuration/AppVersion';
import Button from '@/components/FormElements/Button';
import DownloadIcon from '@/assets/interface-icons/config-download-file.svg';
import DeleteIcon from '@/assets/interface-icons/config-delete-local.svg';
@ -88,7 +101,6 @@ export default {
name: 'ConfigContainer',
data() {
return {
jsonParser: JsonToYaml,
backupId: localStorage[localStorageKeys.BACKUP_ID] || '',
appVersion: process.env.VUE_APP_VERSION,
latestVersion: '',
@ -101,11 +113,12 @@ export default {
sections: function getSections() {
return this.config.sections;
},
yaml() {
return this.jsonParser(this.config);
enableConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
},
components: {
Button,
JsonEditor,
CustomCssEditor,
CloudBackupRestore,
@ -127,7 +140,11 @@ export default {
this.$refs.tabView.activeTabItem(itemToSelect);
},
openRebuildAppModal() {
this.$modal.show(modalNames.REBUILD_APP);
if (this.enableConfig) {
this.$modal.show(modalNames.REBUILD_APP);
} else {
this.unauthorized();
}
},
openAboutModal() {
this.$modal.show(modalNames.ABOUT_APP);
@ -136,7 +153,20 @@ export default {
this.$modal.show(modalNames.LANG_SWITCHER);
},
openExportConfigModal() {
this.$modal.show(modalNames.EXPORT_CONFIG_MENU);
if (this.enableConfig) {
this.$modal.show(modalNames.EXPORT_CONFIG_MENU);
} else {
this.unauthorized();
}
},
openEditConfigTab() {
this.navigateToTab(1);
},
openCloudSyncTab() {
this.navigateToTab(2);
},
openEditCssTab() {
this.navigateToTab(3);
},
/* Checks that the user is sure, then resets site-wide local storage, and reloads page */
resetLocalSettings() {
@ -160,6 +190,9 @@ export default {
if (navToTab && isValidTabIndex(navToTab)) this.navigateToTab(navToTab);
this.$store.commit(StoreKeys.CONF_MENU_INDEX, undefined);
},
unauthorized() {
ErrorHandler('Unauthorized Operation - Config Disabled');
},
},
mounted() {
this.navigateToStartingTab();
@ -180,17 +213,13 @@ pre {
a.config-button, button.config-button {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
margin: 0.25rem auto;
justify-content: flex-end;
font-size: 1.2rem;
background: var(--config-settings-background);
color: var(--config-settings-color);
border: 1px solid var(--config-settings-color);
border-radius: var(--curve-factor);
text-decoration: none;
cursor: pointer;
margin: 0.5rem auto;
min-width: 18rem;
min-width: 15rem;
width: 100%;
svg.button-icon {
path {
@ -199,9 +228,8 @@ a.config-button, button.config-button {
width: 1rem;
height: 1rem;
padding: 0.2rem;
margin-right: 0.5rem;
}
&:hover {
&:hover:not(.disallowed) {
background: var(--config-settings-color);
color: var(--config-settings-background);
svg path {
@ -226,12 +254,6 @@ p.app-version, p.language {
div.code-container {
background: var(--config-code-background);
#conf-yaml span {
font-family: var(--font-monospace), monospace !important;
&.hljs-attr {
font-weight: bold !important;
}
}
.yaml-action-buttons {
position: absolute;
top: 1.5rem;
@ -320,6 +342,13 @@ div.code-container {
display: none;
@include tablet-up { display: block; }
}
p.config-disabled-note {
margin: 0.5rem auto;
padding: 0 0.5rem;
font-weight: bold;
color: var(--warning);
opacity: var(--dimming-factor);
}
p.small-screen-note {
@include phone {
display: block !important;
@ -335,6 +364,10 @@ p.small-screen-note {
<style lang="scss">
.hideTabs .tab__pagination {
display: none !important;
}
.tabs__content {
height: -webkit-fill-available;
height: -moz-available;
@ -364,6 +397,9 @@ p.small-screen-note {
font-weight: bold !important;
color: var(--config-settings-color) !important;
}
&:hover span {
color: var(--config-settings-background) !important;
}
}
}
.tab__nav__items .tab__nav__item.active {
@ -374,11 +410,4 @@ p.small-screen-note {
}
}
#conf-yaml {
background: var(--white);
.hljs-attr {
color: #9c03f5;
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="json-editor-outer">
<div class="json-editor-outer" v-if="allowViewConfig">
<!-- Main JSON editor -->
<v-jsoneditor v-model="jsonData" :options="options" />
<!-- Options raido, and save button -->
@ -8,11 +8,11 @@
:label="$t('config-editor.save-location-label')"
:options="saveOptions"
:initialOption="initialSaveMode"
:disabled="!allowWriteToDisk"
:disabled="!allowWriteToDisk || !allowSaveLocally"
/>
<!-- Save Buttons -->
<div :class="`btn-container ${!isValid ? 'err' : ''}`">
<Button :click="save">
<Button :click="save" :disallow="!allowWriteToDisk && !allowSaveLocally">
{{ $t('config-editor.save-button') }}
</Button>
<Button :click="startPreview">
@ -46,6 +46,7 @@
</p>
<p class="note">{{ $t('config.backup-note') }}</p>
</div>
<AccessError v-else />
</template>
<script>
@ -58,9 +59,9 @@ import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
import configSchema from '@/utils/ConfigSchema.json';
import StoreKeys from '@/utils/StoreMutations';
import { localStorageKeys, serviceEndpoints, modalNames } from '@/utils/defaults';
import { isUserAdmin } from '@/utils/Auth';
import Button from '@/components/FormElements/Button';
import Radio from '@/components/FormElements/Radio';
import AccessError from '@/components/Configuration/AccessError';
export default {
name: 'JsonEditor',
@ -68,6 +69,7 @@ export default {
VJsoneditor,
Button,
Radio,
AccessError,
},
data() {
return {
@ -97,12 +99,23 @@ export default {
isValid() {
return this.errorMessages.length < 1;
},
permissions() {
// Returns: { allowWriteToDisk, allowSaveLocally, allowViewConfig }
return this.$store.getters.permissions;
},
allowWriteToDisk() {
const { appConfig } = this.config;
return appConfig.allowConfigEdit !== false && isUserAdmin();
return this.permissions.allowWriteToDisk;
},
allowSaveLocally() {
return this.permissions.allowSaveLocally;
},
allowViewConfig() {
return this.permissions.allowViewConfig;
},
initialSaveMode() {
return this.allowWriteToDisk ? 'file' : 'local';
if (this.allowWriteToDisk) return 'file';
if (this.allowSaveLocally) return 'local';
return '';
},
},
mounted() {
@ -166,6 +179,10 @@ export default {
},
/* Saves config to local browser storage */
saveConfigLocally() {
if (!this.allowSaveLocally) {
ErrorHandler('Unable to save changes locally, this feature has been disabled');
return;
}
const data = this.jsonData;
if (data.sections) {
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(data.sections));
@ -180,7 +197,7 @@ export default {
if (data.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, data.appConfig.theme);
}
InfoHandler('Config has succesfully been saved in browser storage', InfoKeys.RAW_EDITOR);
InfoHandler('Config has successfully been saved in browser storage', InfoKeys.RAW_EDITOR);
this.showToast(this.$t('config-editor.success-msg-local'), true);
},
/* Clears config from browser storage, only removing relevant items */
@ -277,7 +294,7 @@ p.response-output {
}
p.no-permission-note {
color: var(--config-settings-color);
color: var(--warning);
}
.btn-container {

View File

@ -51,7 +51,9 @@ import Button from '@/components/FormElements/Button';
import RebuildIcon from '@/assets/interface-icons/application-rebuild.svg';
import ReloadIcon from '@/assets/interface-icons/application-reload.svg';
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
import ErrorHandler from '@/utils/ErrorHandler';
import { modalNames, serviceEndpoints } from '@/utils/defaults';
import { isUserAdmin } from '@/utils/Auth';
export default {
name: 'RebuildApp',
@ -79,6 +81,10 @@ export default {
methods: {
/* Calls to the rebuild endpoint, to kickoff the app build */
startBuild() {
if (!this.allowRebuild) { // Double check user is allowed
ErrorHandler('Unable to trigger rebuild, insufficient permission');
return;
}
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
const endpoint = `${baseUrl}${serviceEndpoints.rebuild}`;
this.loading = true;
@ -116,7 +122,10 @@ export default {
},
},
mounted() {
if (this.appConfig.allowConfigEdit === false) {
// Disable rebuild functionality if user not allowed
if (this.appConfig.allowConfigEdit === false
|| this.appConfig.preventWriteToDisk
|| !isUserAdmin()) {
this.allowRebuild = false;
}
},

View File

@ -3,7 +3,7 @@
@click="click ? click() : () => null"
:class="disallow ? 'disallowed': ''"
:type="type || 'button'"
:disabled="disabled"
:disabled="disabled || disallow"
v-tooltip="hoverText"
:title="tooltip"
>
@ -68,7 +68,7 @@ button {
background: var(--background);
border: 1px solid var(--primary);
border-radius: var(--curve-factor);
&:hover:not(:disabled) {
&:hover:not(:disabled):not(.disallowed) {
color: var(--background);
background: var(--primary);
border-color: var(--background);

View File

@ -14,6 +14,7 @@
:name="name"
:id="name"
:placeholder="placeholder"
@keyup.enter="onEnter ? onEnter() : () => {}"
class="input-field"
/>
<p
@ -35,6 +36,7 @@ export default {
name: String, // Required unique ID value, for accessibility
placeholder: String, // Optional placeholder value
description: String, // Optional info paragraph
onEnter: Function,
type: {
default: 'text', // Input type, e.g. text, password, number
type: String,

View File

@ -7,7 +7,7 @@
classes="dashy-modal edit-app-config"
@closed="modalClosed"
>
<div class="edit-app-config-inner">
<div class="edit-app-config-inner" v-if="allowViewConfig">
<h3>{{ $t('interactive-editor.menu.edit-app-config-btn') }}</h3>
<!-- Show caution message -->
<div class="app-config-intro">
@ -35,6 +35,7 @@
<!-- Save Button, lower -->
<SaveCancelButtons :saveClick="saveToState" :cancelClick="cancelEditing" />
</div>
<AccessError v-else />
</modal>
</template>
@ -43,6 +44,7 @@ import FormSchema from '@formschema/native';
import DashySchema from '@/utils/ConfigSchema';
import StoreKeys from '@/utils/StoreMutations';
import { modalNames } from '@/utils/defaults';
import AccessError from '@/components/Configuration/AccessError';
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
export default {
@ -58,6 +60,7 @@ export default {
components: {
FormSchema,
SaveCancelButtons,
AccessError,
},
mounted() {
this.formData = this.appConfig;
@ -66,6 +69,9 @@ export default {
appConfig() {
return this.$store.getters.appConfig;
},
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
},
methods: {
/* When form submitteed, update VueX store with new appConfig, and close modal */

View File

@ -7,7 +7,7 @@
classes="dashy-modal edit-item"
@closed="modalClosed"
>
<div class="edit-item-inner">
<div class="edit-item-inner" v-if="allowViewConfig">
<!-- Title and Item ID -->
<h3 class="title">Edit Item</h3>
<p class="sub-title">Editing {{item.title}} (ID: {{itemId}})</p>
@ -67,6 +67,7 @@
<!-- Save to state button -->
<SaveCancelButtons :saveClick="saveItem" :cancelClick="modalClosed" />
</div>
<AccessError v-else />
</modal>
</template>
@ -74,6 +75,7 @@
import AddIcon from '@/assets/interface-icons/interactive-editor-add.svg';
import BinIcon from '@/assets/interface-icons/interactive-editor-remove.svg';
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
import AccessError from '@/components/Configuration/AccessError';
import Input from '@/components/FormElements/Input';
import Radio from '@/components/FormElements/Radio';
import Select from '@/components/FormElements/Select';
@ -101,13 +103,18 @@ export default {
isNew: Boolean,
parentSectionTitle: String, // If adding new item, which section to add it under
},
computed: {},
computed: {
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
},
components: {
Input,
Radio,
Select,
AddIcon,
BinIcon,
AccessError,
SaveCancelButtons,
},
mounted() {

View File

@ -1,7 +1,7 @@
<template>
<!-- Intro Info -->
<div class="edit-mode-bottom-banner">
<div class="edit-banner-section intro-container">
<div class="edit-banner-section intro-container" v-if="showEditMsg">
<p class="section-sub-title edit-mode-intro l-1">
{{ $t('interactive-editor.menu.edit-mode-subtitle') }}
</p>
@ -9,6 +9,9 @@
{{ $t('interactive-editor.menu.edit-mode-description') }}
</p>
</div>
<div class="edit-banner-section intro-container" v-else>
<AccessError class="no-permission" />
</div>
<div class="edit-banner-section empty-space"></div>
<!-- Save Buttons -->
<div class="edit-banner-section save-buttons-container">
@ -17,6 +20,7 @@
</p>
<Button
:click="saveLocally"
:disallow="!permissions.allowSaveLocally"
v-tooltip="tooltip($t('interactive-editor.menu.save-locally-tooltip'))"
>
{{ $t('interactive-editor.menu.save-locally-btn') }}
@ -24,7 +28,7 @@
</Button>
<Button
:click="writeToDisk"
:disabled="!allowWriteToDisk"
:disallow="!permissions.allowWriteToDisk"
v-tooltip="tooltip($t('interactive-editor.menu.save-disk-tooltip'))"
>
{{ $t('interactive-editor.menu.save-disk-btn') }}
@ -32,6 +36,7 @@
</Button>
<Button
:click="openExportConfigMenu"
:disallow="!permissions.allowViewConfig"
v-tooltip="tooltip($t('interactive-editor.menu.export-config-tooltip'))"
>
{{ $t('interactive-editor.menu.export-config-btn') }}
@ -52,6 +57,7 @@
</p>
<Button
:click="openEditPageInfo"
:disallow="!permissions.allowViewConfig"
v-tooltip="tooltip($t('interactive-editor.menu.edit-page-info-tooltip'))"
>
{{ $t('interactive-editor.menu.edit-page-info-btn') }}
@ -59,6 +65,7 @@
</Button>
<Button
:click="openEditAppConfig"
:disallow="!permissions.allowViewConfig"
v-tooltip="tooltip($t('interactive-editor.menu.edit-app-config-tooltip'))"
>
{{ $t('interactive-editor.menu.edit-app-config-btn') }}
@ -82,7 +89,7 @@ import EditPageInfo from '@/components/InteractiveEditor/EditPageInfo';
import EditAppConfig from '@/components/InteractiveEditor/EditAppConfig';
import { modalNames, localStorageKeys, serviceEndpoints } from '@/utils/defaults';
import ErrorHandler, { InfoHandler } from '@/utils/ErrorHandler';
import { isUserAdmin } from '@/utils/Auth';
import AccessError from '@/components/Configuration/AccessError';
import SaveLocallyIcon from '@/assets/interface-icons/interactive-editor-save-locally.svg';
import SaveToDiskIcon from '@/assets/interface-icons/interactive-editor-save-disk.svg';
@ -103,14 +110,18 @@ export default {
AppConfigIcon,
PageInfoIcon,
EditAppConfig,
AccessError,
},
computed: {
config() {
return this.$store.state.config;
},
allowWriteToDisk() {
const { appConfig } = this.config;
return appConfig.allowConfigEdit !== false && isUserAdmin();
permissions() {
// Returns: { allowWriteToDisk, allowSaveLocally, allowViewConfig }
return this.$store.getters.permissions;
},
showEditMsg() {
return this.permissions.allowWriteToDisk || this.permissions.allowSaveLocally;
},
},
data() {
@ -149,6 +160,10 @@ export default {
localStorage.removeItem(localStorageKeys.CONF_SECTIONS);
},
saveLocally() {
if (!this.permissions.allowSaveLocally) {
ErrorHandler('Unable to save changes locally, this feature has been disabled');
return;
}
const data = this.config;
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(data.sections));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(data.pageInfo));
@ -161,6 +176,10 @@ export default {
this.$store.commit(StoreKeys.SET_EDIT_MODE, false);
},
writeToDisk() {
if (this.config.appConfig.preventWriteToDisk) {
ErrorHandler('Unable to write changed to disk, as this functionality is disabled');
return;
}
// 1. Convert JSON into YAML
const yamlOptions = {};
const yaml = jsYaml.dump(this.config, yamlOptions);
@ -228,11 +247,16 @@ div.edit-mode-bottom-banner {
}
/* Intro-text container */
&.intro-container {
p.edit-mode-intro {
p.edit-mode-intro {
margin: 0;
color: var(--interactive-editor-color);
cursor: default;
}
.no-permission {
margin: 0;
width: auto;
padding: 0 0.5rem;
}
}
/* Button containers */
&.edit-site-config-buttons,
@ -270,7 +294,7 @@ div.edit-mode-bottom-banner {
color: var(--interactive-editor-color);
border-color: var(--interactive-editor-color);
background: var(--interactive-editor-background);
&:hover {
&:hover:not(.disallowed) {
color: var(--interactive-editor-background);
border-color: var(--interactive-editor-color);
background: var(--interactive-editor-color);

View File

@ -4,7 +4,7 @@
:resizable="true" width="50%" height="80%"
classes="dashy-modal edit-page-info"
>
<div class="edit-page-info-inner">
<div class="edit-page-info-inner" v-if="allowViewConfig">
<h3>{{ $t('interactive-editor.menu.edit-page-info-btn') }}</h3>
<FormSchema
:schema="schema"
@ -19,6 +19,7 @@
</Button>
</FormSchema>
</div>
<AccessError v-else />
</modal>
</template>
@ -29,6 +30,7 @@ import StoreKeys from '@/utils/StoreMutations';
import { modalNames } from '@/utils/defaults';
import Button from '@/components/FormElements/Button';
import SaveIcon from '@/assets/interface-icons/save-config.svg';
import AccessError from '@/components/Configuration/AccessError';
export default {
name: 'EditPageInfo',
@ -43,6 +45,7 @@ export default {
FormSchema,
Button,
SaveIcon,
AccessError,
},
mounted() {
this.formData = this.pageInfo;
@ -51,6 +54,9 @@ export default {
pageInfo() {
return this.$store.getters.pageInfo;
},
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
},
methods: {
/* When form submitteed, update VueX store with new pageInfo, and close modal */

View File

@ -4,7 +4,7 @@
:resizable="true" width="50%" height="80%"
classes="dashy-modal edit-section"
>
<div class="edit-section-inner">
<div class="edit-section-inner" v-if="allowViewConfig">
<h3>
{{ $t(`interactive-editor.edit-section.${isAddNew ? 'add' : 'edit'}-section-title`) }}
</h3>
@ -19,6 +19,7 @@
:cancelClick="modalClosed"
/>
</div>
<AccessError v-else />
</modal>
</template>
@ -28,6 +29,7 @@ import StoreKeys from '@/utils/StoreMutations';
import DashySchema from '@/utils/ConfigSchema';
import { modalNames } from '@/utils/defaults';
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
import AccessError from '@/components/Configuration/AccessError';
export default {
name: 'EditSection',
@ -38,6 +40,7 @@ export default {
components: {
SaveCancelButtons,
FormSchema,
AccessError,
},
data() {
return {
@ -71,6 +74,9 @@ export default {
},
};
},
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
},
mounted() {
this.sectionData = this.$store.getters.getSectionByIndex(this.sectionIndex);

View File

@ -4,10 +4,10 @@
:resizable="true"
width="50%"
height="80%"
classes="dashy-modal edit-item"
classes="dashy-modal export-modal"
@closed="modalClosed"
>
<div class="export-config-inner">
<div class="export-config-inner" v-if="allowViewConfig">
<!-- Download and Copy to CLipboard Buttons -->
<h3>{{ $t('interactive-editor.export.export-title') }}</h3>
<div class="download-button-container">
@ -26,6 +26,7 @@
<h3>{{ $t('interactive-editor.export.view-title') }}</h3>
<tree-view :data="config" class="config-tree-view" />
</div>
<AccessError v-else />
</modal>
</template>
@ -34,6 +35,7 @@ import JsYaml from 'js-yaml';
import Button from '@/components/FormElements/Button';
import StoreKeys from '@/utils/StoreMutations';
import { modalNames } from '@/utils/defaults';
import AccessError from '@/components/Configuration/AccessError';
import DownloadConfigIcon from '@/assets/interface-icons/config-download-file.svg';
import CopyConfigIcon from '@/assets/interface-icons/interactive-editor-copy-clipboard.svg';
import { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
@ -42,6 +44,7 @@ export default {
name: 'ExportConfigMenu',
components: {
Button,
AccessError,
CopyConfigIcon,
DownloadConfigIcon,
},
@ -55,6 +58,9 @@ export default {
config() {
return this.$store.state.config;
},
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
},
methods: {
convertJsonToYaml() {
@ -123,5 +129,8 @@ export default {
}
}
}
.export-modal {
background: var(--interactive-editor-background);
}
</style>

View File

@ -2,7 +2,7 @@
<modal
:name="modalName" @closed="close"
:resizable="true" width="40%" height="40%" classes="dashy-modal">
<div class="move-menu-inner">
<div class="move-menu-inner" v-if="allowViewConfig">
<!-- Title and item ID -->
<h3 class="move-title">Move or Copy Item</h3>
<p class="item-id">Editing {{ itemId }}</p>
@ -30,6 +30,7 @@
<!-- Save and cancel buttons -->
<SaveCancelButtons :saveClick="save" :cancelClick="close" />
</div>
<AccessError v-else />
</modal>
</template>
@ -37,6 +38,7 @@
import Select from '@/components/FormElements/Select';
import Radio from '@/components/FormElements/Radio';
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
import AccessError from '@/components/Configuration/AccessError';
import StoreKeys from '@/utils/StoreMutations';
import { modalNames } from '@/utils/defaults';
@ -45,6 +47,7 @@ export default {
components: {
Select,
Radio,
AccessError,
SaveCancelButtons,
},
props: {
@ -83,6 +86,9 @@ export default {
});
return sectionName;
},
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
},
mounted() {
this.selectedSection = this.currentSection;

View File

@ -29,7 +29,7 @@
</li>
</ul>
<!-- Edit Options -->
<ul class="menu-section">
<ul class="menu-section" v-bind:class="{ disabled: !isEditAllowed }">
<li class="section-title">
{{ $t('context-menus.item.options-section-title') }}
</li>
@ -85,6 +85,9 @@ export default {
isEditMode() {
return this.$store.state.editMode;
},
isEditAllowed() {
return this.$store.getters.permissions.allowViewConfig;
},
},
methods: {
/* Called on item click, emits an event up to Item */
@ -93,13 +96,19 @@ export default {
this.$emit('launchItem', target);
},
openSettings() {
this.$emit('openItemSettings');
if (this.isEditAllowed) {
this.$emit('openItemSettings');
}
},
openMoveMenu() {
this.$emit('openMoveItemMenu');
if (this.isEditAllowed) {
this.$emit('openMoveItemMenu');
}
},
openDeleteItem() {
this.$emit('openDeleteItem');
if (this.isEditAllowed) {
this.$emit('openDeleteItem');
}
},
},
};
@ -149,6 +158,13 @@ div.context-menu {
path { fill: currentColor; }
}
}
&.disabled li:not(.section-title) {
cursor: not-allowed;
opacity: var(--dimming-factor);
&:hover {
background: var(--context-menu-background);
}
}
}
}

View File

@ -244,6 +244,10 @@ export default {
},
/* Navigate to the section's single-section view page */
navigateToSection() {
if (!this.title) {
ErrorHandler('Cannot open section without a valid name');
return;
}
const parse = (section) => section.replace(' ', '-').toLowerCase().trim();
const sectionIdentifier = parse(this.title);
router.push({ path: `/home/${sectionIdentifier}` });

View File

@ -7,7 +7,7 @@
v-tooltip="tooltip($t('settings.config-launcher-tooltip'))" />
<IconInteractiveEditor @click="startInteractiveEditor()" tabindex="-2"
v-tooltip="tooltip(enterEditModeTooltip)"
:class="isEditMode ? 'disabled' : ''" />
:class="(isEditMode || !isEditAllowed) ? 'disabled' : ''" />
<IconViewMode @click="openChangeViewMenu()" tabindex="-2"
v-tooltip="tooltip($t('alternate-views.alternate-view-heading'))" />
</div>
@ -91,8 +91,12 @@ export default {
isEditMode() {
return this.$store.state.editMode;
},
isEditAllowed() {
return this.$store.getters.permissions.allowViewConfig;
},
/* Tooltip text for Edit Mode button, to change depending on it in edit mode */
enterEditModeTooltip() {
if (!this.isEditAllowed) return 'Config editor not available';
return this.$t(
`interactive-editor.menu.${this.isEditMode
? 'edit-mode-subtitle' : 'start-editing-tooltip'}`,
@ -126,7 +130,7 @@ export default {
this.viewSwitcherOpen = false;
},
startInteractiveEditor() {
if (!this.isEditMode) {
if (!this.isEditMode && this.isEditAllowed) {
this.$store.commit(Keys.SET_EDIT_MODE, true);
}
},

View File

@ -39,6 +39,7 @@ export default {
filters: {},
methods: {
processData(alertData) {
const round = (num) => ((num && typeof num === 'number') ? Math.round(num) : num);
if (!alertData || alertData.length === 0) {
this.noResults = true;
} else {
@ -51,8 +52,9 @@ export default {
lasted: alert[1] ? getTimeDifference(alert[0] * 1000, alert[1] * 1000) : 'Ongoing',
severity: alert[2],
category: alert[3],
value: alert[5],
minMax: `Min: ${alert[4]}%<br>Avg: ${alert[5]}%<br>Max: ${alert[6]}%`,
value: round(alert[5]),
minMax: `Min: ${round(alert[4])}%<br>Avg: `
+ `${round(alert[5])}%<br>Max: ${round(alert[6])}%`,
});
});
this.alerts = alerts;

View File

@ -0,0 +1,83 @@
<template>
<div class="glances-temp-wrapper" v-if="tempData">
<div class="temp-row" v-for="sensor in tempData" :key="sensor.label">
<p class="label">{{ sensor.label | formatLbl }}</p>
<p :class="`temp range-${sensor.color}`">{{ sensor.value | formatVal }}</p>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import GlancesMixin from '@/mixins/GlancesMixin';
import { capitalize, fahrenheitToCelsius } from '@/utils/MiscHelpers';
export default {
mixins: [WidgetMixin, GlancesMixin],
data() {
return {
tempData: null,
noResults: false,
};
},
computed: {
endpoint() {
return this.makeGlancesUrl('sensors');
},
},
filters: {
formatLbl(lbl) {
return capitalize(lbl);
},
formatVal(val) {
return `${Math.round(val)}°C`;
},
},
methods: {
getTempColor(temp) {
if (temp <= 50) return 'green';
if (temp > 50 && temp < 75) return 'yellow';
if (temp >= 75) return 'red';
return 'grey';
},
processData(sensorData) {
const results = [];
sensorData.forEach((sensor) => {
const tempC = sensor.unit === 'F' ? fahrenheitToCelsius(sensor.value) : sensor.value;
results.push({
label: sensor.label,
value: tempC,
color: this.getTempColor(tempC),
});
});
this.tempData = results;
},
},
};
</script>
<style scoped lang="scss">
.glances-temp-wrapper {
.temp-row {
display: flex;
align-items: center;
justify-content: space-between;
p.label {
margin: 0.5rem 0;
color: var(--widget-text-color);
}
p.temp {
margin: 0.5rem 0;
font-size: 1.5rem;
font-weight: bold;
&.range-green { color: var(--success); }
&.range-yellow { color: var(--warning); }
&.range-red { color: var(--danger); }
&.range-grey { color: var(--medium-grey); }
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
}
</style>

View File

@ -0,0 +1,28 @@
<template>
<div class="image-widget">
<img :src="imagePath" class="embedded-image" />
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
export default {
mixins: [WidgetMixin],
computed: {
imagePath() {
if (!this.options.imagePath) this.error('You must specify an imagePath');
return this.options.imagePath;
},
},
};
</script>
<style scoped lang="scss">
.image-widget {
img.embedded-image {
max-width: 100%;
margin: 0.2rem auto;
}
}
</style>

View File

@ -209,6 +209,13 @@
@error="handleError"
:ref="widgetRef"
/>
<GlCpuTemp
v-else-if="widgetType === 'gl-cpu-temp'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<HealthChecks
v-else-if="widgetType === 'health-checks'"
:options="widgetOptions"
@ -223,6 +230,13 @@
@error="handleError"
:ref="widgetRef"
/>
<ImageWidget
v-else-if="widgetType === 'image'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Jokes
v-else-if="widgetType === 'joke'"
:options="widgetOptions"
@ -413,8 +427,10 @@ export default {
GlNetworkInterfaces: () => import('@/components/Widgets/GlNetworkInterfaces.vue'),
GlNetworkTraffic: () => import('@/components/Widgets/GlNetworkTraffic.vue'),
GlSystemLoad: () => import('@/components/Widgets/GlSystemLoad.vue'),
GlCpuTemp: () => import('@/components/Widgets/GlCpuTemp.vue'),
HealthChecks: () => import('@/components/Widgets/HealthChecks.vue'),
IframeWidget: () => import('@/components/Widgets/IframeWidget.vue'),
ImageWidget: () => import('@/components/Widgets/ImageWidget.vue'),
Jokes: () => import('@/components/Widgets/Jokes.vue'),
NdCpuHistory: () => import('@/components/Widgets/NdCpuHistory.vue'),
NdLoadHistory: () => import('@/components/Widgets/NdLoadHistory.vue'),
@ -446,6 +462,9 @@ export default {
errorMsg: null,
}),
computed: {
appConfig() {
return this.$store.getters.appConfig;
},
/* Returns the widget type, shows error if not specified */
widgetType() {
if (!this.widget.type) {
@ -457,7 +476,7 @@ export default {
/* Returns users specified widget options, or empty object */
widgetOptions() {
const options = this.widget.options || {};
const useProxy = !!this.widget.useProxy;
const useProxy = this.appConfig.widgetsAlwaysUseProxy || !!this.widget.useProxy;
const updateInterval = this.widget.updateInterval !== undefined
? this.widget.updateInterval : null;
return { useProxy, updateInterval, ...options };

View File

@ -1,4 +1,4 @@
/* eslint-disable no-param-reassign */
/* eslint-disable no-param-reassign, prefer-destructuring */
import Vue from 'vue';
import Vuex from 'vuex';
import Keys from '@/utils/StoreMutations';
@ -7,6 +7,7 @@ import { componentVisibility } from '@/utils/ConfigHelpers';
import { applyItemId } from '@/utils/SectionHelpers';
import filterUserSections from '@/utils/CheckSectionVisibility';
import { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
import { isUserAdmin } from '@/utils/Auth';
Vue.use(Vuex);
@ -63,6 +64,34 @@ const store = new Vuex.Store({
visibleComponents(state, getters) {
return componentVisibility(getters.appConfig);
},
/* Make config read/ write permissions object */
permissions(state, getters) {
const appConfig = getters.appConfig;
const perms = {
allowWriteToDisk: true,
allowSaveLocally: true,
allowViewConfig: true,
};
// Disable saving changes locally, only
if (appConfig.preventLocalSave) {
perms.allowSaveLocally = false;
}
// Disable saving changes to disk, only
if (appConfig.preventWriteToDisk || !isUserAdmin) {
perms.allowWriteToDisk = false;
}
// Legacy Option: Will be removed in V 2.1.0
if (appConfig.allowConfigEdit === false) {
perms.allowWriteToDisk = false;
}
// Disable everything
if (appConfig.disableConfiguration) {
perms.allowWriteToDisk = false;
perms.allowSaveLocally = false;
perms.allowViewConfig = false;
}
return perms;
},
// eslint-disable-next-line arrow-body-style
getSectionByIndex: (state, getters) => (index) => {
return getters.sections[index];

View File

@ -31,7 +31,7 @@ html {
box-shadow: 0 40px 70px -2px hsl(0deg 0% 0% / 60%), 1px 1px 6px var(--primary) !important;
min-width: 350px;
min-height: 200px;
background: var(--background-darker);
@include phone {
left: 0.5rem !important;
right: 0.5rem !important;

View File

@ -39,6 +39,10 @@ const getUsers = () => {
* @returns {String} The hashed token
*/
const generateUserToken = (user) => {
if (!user.user || !user.hash) {
ErrorHandler('Invalid user object. Must have `user` and `hash` parameters');
return undefined;
}
const strAndUpper = (input) => input.toString().toUpperCase();
const sha = sha256(strAndUpper(user.user) + strAndUpper(user.hash));
return strAndUpper(sha);

View File

@ -209,6 +209,12 @@
"default": false,
"description": "If set to true, will keep apps opened in the workspace open in the background. Useful for switching between sites, but comes at the cost of performance"
},
"widgetsAlwaysUseProxy": {
"title": "Widgets always use proxy",
"type": "boolean",
"default": false,
"description": "If set to true, the useProxy option for widgets will always be applied without having to specify it for each widget"
},
"webSearch": {
"title": "Web Search",
"type": "object",
@ -439,6 +445,24 @@
"default": "false",
"description": "If set to true, a loading screen will be shown"
},
"preventWriteToDisk": {
"title": "Prevent saving config to disk",
"type": "boolean",
"default": false,
"description": "If set to true, no users will not be able to save config changes to disk through the UI"
},
"preventLocalSave": {
"title": "Prevent saving config to local storage",
"type": "boolean",
"default": false,
"description": "If set to true, no users will not be able to save config changes to the browser's local storage"
},
"disableConfiguration": {
"title": "Disable all UI Config",
"type": "boolean",
"default": false,
"description": "If set to true, no users will be able to view or edit the config through the UI"
},
"allowConfigEdit": {
"title": "Allow Config Editing",
"type": "boolean",

View File

@ -1,87 +0,0 @@
import { typeOf } from 'remedial';
const trimWhitespace = (input) => input.split('\n').map(x => x.trimRight()).join('\n');
const throwError = (msg) => {
throw new Error(`Error in Json to YAML conversion: ${msg}`);
};
/* A function that converts valid JSON into valid YAML */
const stringify = (data) => {
let indentLevel = '';
const handlers = {
undefined() {
return 'null';
},
null() {
return 'null';
},
number(x) {
return x;
},
boolean(x) {
return x ? 'true' : 'false';
},
string(x) {
return JSON.stringify(x);
},
array(x) {
let output = '';
if (x.length === 0) {
output += '[]';
return output;
}
indentLevel = indentLevel.replace(/$/, ' ');
x.forEach((y) => {
const handler = handlers[typeOf(y)];
if (!handler) throwError(typeOf(y));
output += `\n${indentLevel}- ${handler(y, true)}`;
});
indentLevel = indentLevel.replace(/ {2}/, '');
return output;
},
object(x, inArray, rootNode) {
let output = '';
if (Object.keys(x).length === 0) {
output += '{}';
return output;
}
if (!rootNode) {
indentLevel = indentLevel.replace(/$/, ' ');
}
Object.keys(x).forEach((k, i) => {
const val = x[k];
const handler = handlers[typeOf(val)];
if (typeof val === 'undefined') {
return;
}
if (!handler) throwError(typeOf(val));
if (!(inArray && i === 0)) {
output += `\n${indentLevel}`;
}
output += `${k}: ${handler(val)}`;
});
indentLevel = indentLevel.replace(/ {2}/, '');
return output;
},
function() {
return '[object Function]';
},
};
return trimWhitespace(`${handlers[typeOf(data)](data, true, true)}\n`);
};
export default stringify;

View File

@ -145,6 +145,11 @@ export const getValueFromCss = (colorVar) => {
return cssProps.getPropertyValue(`--${colorVar}`).trim();
};
/* Given a temperature in Fahrenheit, returns value in Celsius */
export const fahrenheitToCelsius = (fahrenheit) => {
return Math.round(((fahrenheit - 32) * 5) / 9);
};
/* Given a currency code, return the corresponding unicode symbol */
export const findCurrencySymbol = (currencyCode) => {
const code = currencyCode.toUpperCase().trim();

View File

@ -7,7 +7,8 @@ export const shouldBeVisible = (routeName) => !hideFurnitureOn.includes(routeNam
/* Based on section title, item name and index, return a string value for ID */
const makeItemId = (sectionStr, itemStr, index) => {
const charSum = sectionStr.split('').map((a) => a.charCodeAt(0)).reduce((x, y) => x + y);
const sectionTitle = sectionStr || `unlabeledSec_${Math.random()}`;
const charSum = sectionTitle.split('').map((a) => a.charCodeAt(0)).reduce((x, y) => x + y);
const newItemStr = itemStr || `unknown_${Math.random()}`;
const itemTitleStr = newItemStr.replace(/\s+/g, '-').replace(/[^a-zA-Z ]/g, '').toLowerCase();
return `${index}_${charSum}_${itemTitleStr}`;

View File

@ -1,21 +1,27 @@
<template>
<pre><code>{{ jsonParser(config) }}</code></pre>
<pre v-if="allowViewConfig"><code>{{ yamlConfig }}</code></pre>
<AccessError v-else />
</template>
<script>
import JsonToYaml from '@/utils/JsonToYaml';
import JsYaml from 'js-yaml';
import AccessError from '@/components/Configuration/AccessError';
export default {
name: 'DownloadConfig',
components: {
AccessError,
},
computed: {
config() {
return this.$store.state.config;
},
},
data() {
return {
jsonParser: JsonToYaml,
};
yamlConfig() {
return JsYaml.dump(this.config);
},
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
},
};
@ -23,8 +29,9 @@ export default {
<style scoped lang="scss">
pre {
background: var(--code-editor-background);
color: var(--code-editor-color);
margin: 0;
padding: 1rem;
color: var(--code-editor-color);
background: var(--code-editor-background);
}
</style>

View File

@ -149,7 +149,7 @@ export default {
let sectionToReturn;
const parse = (section) => section.replaceAll(' ', '-').toLowerCase().trim();
allSections.forEach((section) => {
if (parse(sectionTitle) === parse(section.name)) {
if (parse(sectionTitle) === parse(section.name || '')) {
sectionToReturn = [section];
}
});

View File

@ -21,15 +21,15 @@
<!-- Main login form -->
<form class="login-form" v-if="(!isUserAlreadyLoggedIn) && isAuthenticationEnabled">
<h2 class="login-title">{{ $t('login.title') }}</h2>
<Input
<Input type="text"
v-model="username"
type="text"
:onEnter="submitLogin"
:label="$t('login.username-label')"
class="login-field username"
/>
<Input
<Input type="password"
v-model="password"
type="password"
:onEnter="submitLogin"
:label="$t('login.password-label')"
class="login-field password"
/>
@ -38,6 +38,7 @@
v-model="timeout"
:selectOnTab="true"
:options="dropDownMenu"
:map-keydown="(map) => ({ ...map, 13: () => this.submitLogin() })"
class="login-time-dropdown"
/>
<Button class="login-button" :click="submitLogin">