+
+
+ Some configuration features have been disabled by your administrator
+
+
+ You are using a very small screen, and some screens in this menu may not be optimal
+
{{ $t('config.backup-note') }}
@@ -51,13 +63,13 @@
-
+
-
+
-
+
@@ -65,15 +77,16 @@
+
+
diff --git a/src/components/Widgets/ImageWidget.vue b/src/components/Widgets/ImageWidget.vue
new file mode 100644
index 00000000..a556810f
--- /dev/null
+++ b/src/components/Widgets/ImageWidget.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/src/components/Widgets/WidgetBase.vue b/src/components/Widgets/WidgetBase.vue
index b03967f4..63b13be5 100644
--- a/src/components/Widgets/WidgetBase.vue
+++ b/src/components/Widgets/WidgetBase.vue
@@ -209,6 +209,13 @@
@error="handleError"
:ref="widgetRef"
/>
+
+
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 };
diff --git a/src/store.js b/src/store.js
index fe455e51..95ae846c 100644
--- a/src/store.js
+++ b/src/store.js
@@ -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];
diff --git a/src/styles/global-styles.scss b/src/styles/global-styles.scss
index 710e78ad..7e4ef40c 100644
--- a/src/styles/global-styles.scss
+++ b/src/styles/global-styles.scss
@@ -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;
diff --git a/src/utils/Auth.js b/src/utils/Auth.js
index fb0eab2c..df9f3478 100644
--- a/src/utils/Auth.js
+++ b/src/utils/Auth.js
@@ -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);
diff --git a/src/utils/ConfigSchema.json b/src/utils/ConfigSchema.json
index 7b1ca073..dab1e454 100644
--- a/src/utils/ConfigSchema.json
+++ b/src/utils/ConfigSchema.json
@@ -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",
diff --git a/src/utils/JsonToYaml.js b/src/utils/JsonToYaml.js
deleted file mode 100644
index c855f5d0..00000000
--- a/src/utils/JsonToYaml.js
+++ /dev/null
@@ -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;
diff --git a/src/utils/MiscHelpers.js b/src/utils/MiscHelpers.js
index fd34ada9..cc26403e 100644
--- a/src/utils/MiscHelpers.js
+++ b/src/utils/MiscHelpers.js
@@ -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();
diff --git a/src/utils/SectionHelpers.js b/src/utils/SectionHelpers.js
index 28082bfb..21f392d8 100644
--- a/src/utils/SectionHelpers.js
+++ b/src/utils/SectionHelpers.js
@@ -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}`;
diff --git a/src/views/DownloadConfig.vue b/src/views/DownloadConfig.vue
index ef3bf58c..e056daa0 100644
--- a/src/views/DownloadConfig.vue
+++ b/src/views/DownloadConfig.vue
@@ -1,21 +1,27 @@
- {{ jsonParser(config) }}
+ {{ yamlConfig }}
+