Merge branch 'master' of github.com:Lissy93/dashy into FEATURE/sub-items

This commit is contained in:
Alicia Sykes
2022-02-15 11:12:42 +00:00
46 changed files with 963 additions and 309 deletions

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

@@ -259,6 +259,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 };