Merge branch 'FEAT/Dashy-V3' into FEATURE/environment-variables
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
<!-- License -->
|
||||
<h3>{{ $t('app-info.license') }}</h3>
|
||||
{{ $t('app-info.license-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>
|
||||
Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © {{new Date().getFullYear()}}.<br>
|
||||
{{ $t('app-info.licence-third-party') }} <a href="https://github.com/Lissy93/dashy/blob/master/.github/LEGAL.md">{{ $t('app-info.licence-third-party-link') }}</a>.<br>
|
||||
{{ $t('app-info.list-contributors') }} <a href="https://github.com/Lissy93/dashy/blob/master/docs/credits.md">{{ $t('app-info.list-contributors-link') }}</a>.
|
||||
<!-- App Version -->
|
||||
|
||||
@@ -155,12 +155,23 @@ export default {
|
||||
},
|
||||
/* When restored data is revieved, then save to local storage, and apply it in state */
|
||||
applyRestoredData(config, backupId) {
|
||||
// Store restored data in local storage
|
||||
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
|
||||
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
|
||||
if (config.appConfig.theme) {
|
||||
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
|
||||
const isSubPage = !!this.$store.state.currentConfigInfo.confId;
|
||||
if (isSubPage) { // Apply to sub-page only
|
||||
const subConfigId = this.$store.state.currentConfigInfo.confId;
|
||||
const sectionStorageKey = `${localStorageKeys.CONF_SECTIONS}-${subConfigId}`;
|
||||
const pageInfoStorageKey = `${localStorageKeys.PAGE_INFO}-${subConfigId}`;
|
||||
const themeStoreKey = `${localStorageKeys.THEME}-${subConfigId}`;
|
||||
localStorage.setItem(sectionStorageKey, JSON.stringify(config.sections));
|
||||
localStorage.setItem(pageInfoStorageKey, JSON.stringify(config.pageInfo));
|
||||
localStorage.setItem(themeStoreKey, config.appConfig.theme);
|
||||
} else { // Apply to main config
|
||||
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
|
||||
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
|
||||
localStorage.setItem(localStorageKeys.CONF_PAGES, JSON.stringify(config.pages || []));
|
||||
if (config.appConfig.theme) {
|
||||
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
|
||||
}
|
||||
}
|
||||
// Save hashed token in local storage
|
||||
this.setBackupIdLocally(backupId, this.restorePassword);
|
||||
|
||||
@@ -115,7 +115,9 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.jsonData = this.config;
|
||||
const jsonData = { ...this.config };
|
||||
jsonData.sections = jsonData.sections.map(({ filteredItems, ...section }) => section);
|
||||
this.jsonData = jsonData;
|
||||
if (!this.allowWriteToDisk) this.saveMode = 'local';
|
||||
},
|
||||
methods: {
|
||||
@@ -141,7 +143,11 @@ export default {
|
||||
this.$modal.hide(modalNames.CONF_EDITOR);
|
||||
},
|
||||
writeToDisk() {
|
||||
this.writeConfigToDisk(this.config);
|
||||
const newData = this.jsonData;
|
||||
this.writeConfigToDisk(newData);
|
||||
// this.$store.commit(StoreKeys.SET_APP_CONFIG, newData.appConfig);
|
||||
this.$store.commit(StoreKeys.SET_PAGE_INFO, newData.pageInfo);
|
||||
this.$store.commit(StoreKeys.SET_SECTIONS, newData.sections);
|
||||
},
|
||||
saveLocally() {
|
||||
const msg = this.$t('interactive-editor.menu.save-locally-warning');
|
||||
|
||||
@@ -94,6 +94,7 @@ export default {
|
||||
const raw = rawAppConfig;
|
||||
const isEmptyObject = (obj) => (typeof obj === 'object' && Object.keys(obj).length === 0);
|
||||
const isEmpty = (value) => (value === undefined || isEmptyObject(value));
|
||||
|
||||
// Delete empty values
|
||||
Object.keys(raw).forEach(key => {
|
||||
if (isEmpty(raw[key])) delete raw[key];
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
:target="anchorTarget"
|
||||
:class="`item ${makeClassList}`"
|
||||
v-tooltip="getTooltipOptions()"
|
||||
rel="noopener noreferrer" tabindex="0"
|
||||
:rel="`${item.rel || 'noopener noreferrer'}`"
|
||||
tabindex="0"
|
||||
:id="`link-${item.id}`"
|
||||
:style="customStyle"
|
||||
>
|
||||
@@ -258,6 +259,9 @@ export default {
|
||||
overflow: hidden;
|
||||
span.text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
</div>
|
||||
<!-- Modal for opening in modal view -->
|
||||
<IframeModal
|
||||
:ref="`iframeModal`"
|
||||
:name="`iframeModal`"
|
||||
:ref="`iframeModal-${groupId}`"
|
||||
:name="`iframeModal-${groupId}`"
|
||||
@closed="$emit('itemClicked')"
|
||||
/>
|
||||
<!-- Edit item menu -->
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
methods: {
|
||||
/* Opens the iframe modal */
|
||||
triggerModal(url) {
|
||||
this.$refs.iframeModal.show(url);
|
||||
this.$refs[`iframeModal-${this.groupId}`].show(url);
|
||||
},
|
||||
/* Sorts items alphabetically using the title attribute */
|
||||
sortAlphabetically(items) {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<template>
|
||||
<!-- User Footer -->
|
||||
<footer v-if="text && text !== '' && visible" v-html="text"></footer>
|
||||
<!-- Default Footer -->
|
||||
<footer v-else-if="visible">
|
||||
<span v-if="$store.state.currentConfigInfo" class="path-to-config">
|
||||
Using: {{ $store.state.currentConfigInfo.confPath }}
|
||||
</span>
|
||||
<span>
|
||||
{{ $t('footer.dev-by') }} <a :href="authorUrl">{{authorName}}</a>.
|
||||
{{ $t('footer.licensed-under') }} <a :href="licenseUrl">{{license}}</a>
|
||||
{{ showCopyright? '©': '' }} {{date}}.
|
||||
{{ $t('footer.get-the') }} <a :href="repoUrl">{{ $t('footer.source-code') }}</a>.
|
||||
<footer v-if="visible">
|
||||
<!-- User-defined footer -->
|
||||
<span v-if="text" v-html="text"></span>
|
||||
<!-- Default footer -->
|
||||
<span v-else>
|
||||
<a :href="defaultInfo.projectUrl">Dashy</a> is free & open source
|
||||
- licensed under <a :href="defaultInfo.licenseUrl">{{defaultInfo.license}}</a>,
|
||||
© <a :href="defaultInfo.authorUrl">{{defaultInfo.authorName}}</a> {{defaultInfo.date}}.
|
||||
Get support on GitHub, at <a :href="defaultInfo.repoUrl">{{defaultInfo.repoName}}</a>.
|
||||
</span>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -23,13 +20,20 @@ export default {
|
||||
name: 'Footer',
|
||||
props: {
|
||||
text: String,
|
||||
authorName: { type: String, default: 'Alicia Sykes' },
|
||||
authorUrl: { type: String, default: 'https://aliciasykes.com' },
|
||||
license: { type: String, default: 'MIT' },
|
||||
licenseUrl: { type: String, default: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17' },
|
||||
date: { type: String, default: `${new Date().getFullYear()}` },
|
||||
showCopyright: { type: Boolean, default: true },
|
||||
repoUrl: { type: String, default: 'https://github.com/lissy93/dashy' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaultInfo: {
|
||||
authorName: 'Alicia Sykes',
|
||||
authorUrl: 'https://as93.net',
|
||||
license: 'MIT',
|
||||
licenseUrl: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17',
|
||||
date: `${new Date().getFullYear()}`,
|
||||
repoUrl: 'https://github.com/lissy93/dashy',
|
||||
repoName: 'Lissy93/Dashy',
|
||||
projectUrl: 'https://dashy.to',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible() {
|
||||
@@ -56,7 +60,7 @@ footer {
|
||||
display: none;
|
||||
}
|
||||
span.path-to-config {
|
||||
float: right;
|
||||
float: left;
|
||||
font-size: 0.75rem;
|
||||
margin: 0.1rem 0.5rem 0 0;
|
||||
opacity: var(--dimming-factor);
|
||||
|
||||
@@ -66,7 +66,7 @@ export default {
|
||||
span.subtitle {
|
||||
color: var(--heading-text-color);
|
||||
font-style: italic;
|
||||
text-shadow: 1px 1px 2px #130f23;
|
||||
text-shadow: 1px 1px 2px #130f2347;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
img.site-logo {
|
||||
|
||||
@@ -95,7 +95,8 @@ export default {
|
||||
},
|
||||
/* If configured, launch specific app when hotkey pressed */
|
||||
handleHotKey(key) {
|
||||
const usersHotKeys = this.getCustomKeyShortcuts();
|
||||
const sections = this.$store.getters.sections || [];
|
||||
const usersHotKeys = this.getCustomKeyShortcuts(sections);
|
||||
usersHotKeys.forEach((hotkey) => {
|
||||
if (hotkey.hotkey === parseInt(key, 10)) {
|
||||
if (hotkey.url) window.open(hotkey.url, '_blank');
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:value="$store.getters.theme"
|
||||
class="theme-dropdown"
|
||||
:tabindex="-2"
|
||||
@input="themeChanged"
|
||||
@input="themeChangedInUI"
|
||||
/>
|
||||
</div>
|
||||
<IconPalette
|
||||
@@ -28,18 +28,13 @@
|
||||
<script>
|
||||
|
||||
import CustomThemeMaker from '@/components/Settings/CustomThemeMaker';
|
||||
import {
|
||||
LoadExternalTheme,
|
||||
ApplyLocalTheme,
|
||||
ApplyCustomVariables,
|
||||
} from '@/utils/ThemeHelper';
|
||||
import Defaults, { localStorageKeys } from '@/utils/defaults';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import IconPalette from '@/assets/interface-icons/config-color-palette.svg';
|
||||
import ThemingMixin from '@/mixins/ThemingMixin';
|
||||
|
||||
export default {
|
||||
name: 'ThemeSelector',
|
||||
mixins: [ThemingMixin],
|
||||
props: {
|
||||
hidePallete: Boolean,
|
||||
},
|
||||
@@ -47,101 +42,16 @@ export default {
|
||||
CustomThemeMaker,
|
||||
IconPalette,
|
||||
},
|
||||
watch: {
|
||||
/* When theme in VueX store changes, then update theme */
|
||||
themeFromStore(newTheme) {
|
||||
this.selectedTheme = newTheme;
|
||||
this.updateTheme(newTheme);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTheme: '',
|
||||
themeConfiguratorOpen: false, // Control the opening of theme config popup
|
||||
themeHelper: new LoadExternalTheme(),
|
||||
ApplyLocalTheme,
|
||||
ApplyCustomVariables,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/* Get appConfig from store */
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
/* Get users theme from store */
|
||||
themeFromStore() {
|
||||
return this.$store.getters.theme;
|
||||
},
|
||||
/* Combines all theme names (builtin and user defined) together */
|
||||
themeNames: function themeNames() {
|
||||
const externalThemeNames = Object.keys(this.externalThemes);
|
||||
const specialThemes = ['custom'];
|
||||
return [...this.extraThemeNames, ...externalThemeNames,
|
||||
...Defaults.builtInThemes, ...specialThemes];
|
||||
},
|
||||
extraThemeNames() {
|
||||
const userThemes = this.appConfig.cssThemes || [];
|
||||
if (typeof userThemes === 'string') return [userThemes];
|
||||
return userThemes;
|
||||
},
|
||||
/* Returns an array of links to external CSS from the Config */
|
||||
externalThemes() {
|
||||
const availibleThemes = {};
|
||||
if (this.appConfig && this.appConfig.externalStyleSheet) {
|
||||
const externals = this.appConfig.externalStyleSheet;
|
||||
if (Array.isArray(externals)) {
|
||||
externals.forEach((ext, i) => {
|
||||
availibleThemes[`External Stylesheet ${i + 1}`] = ext;
|
||||
});
|
||||
} else if (typeof externals === 'string') {
|
||||
availibleThemes['External Stylesheet'] = this.appConfig.externalStyleSheet;
|
||||
} else {
|
||||
ErrorHandler('External stylesheets must be of type string or string[]');
|
||||
}
|
||||
}
|
||||
// availibleThemes.Default = '#';
|
||||
return availibleThemes;
|
||||
},
|
||||
},
|
||||
computed: {},
|
||||
mounted() {
|
||||
const initialTheme = this.getInitialTheme();
|
||||
this.selectedTheme = initialTheme;
|
||||
// Quicker loading, if the theme is local we can apply it immidiatley
|
||||
if (this.isThemeLocal(initialTheme)) {
|
||||
this.updateTheme(initialTheme);
|
||||
}
|
||||
|
||||
// If it's an external stylesheet, then wait for promise to resolve
|
||||
if (this.externalThemes && Object.entries(this.externalThemes).length > 0) {
|
||||
const added = Object.keys(this.externalThemes).map(
|
||||
name => this.themeHelper.add(name, this.externalThemes[name]),
|
||||
);
|
||||
// Once, added, then apply users initial theme
|
||||
Promise.all(added).then(() => {
|
||||
this.updateTheme(initialTheme);
|
||||
});
|
||||
}
|
||||
this.initializeTheme();
|
||||
},
|
||||
methods: {
|
||||
/* Called when dropdown changed
|
||||
* Updates store, which will in turn update theme through watcher
|
||||
*/
|
||||
themeChanged() {
|
||||
const pageId = this.$store.state.currentConfigInfo?.pageId || null;
|
||||
this.$store.commit(Keys.SET_THEME, { theme: this.selectedTheme, pageId });
|
||||
this.updateTheme(this.selectedTheme);
|
||||
},
|
||||
/* Returns the initial theme */
|
||||
getInitialTheme() {
|
||||
const localTheme = localStorage[localStorageKeys.THEME];
|
||||
if (localTheme && localTheme !== 'undefined') return localTheme;
|
||||
return this.appConfig.theme || Defaults.theme;
|
||||
},
|
||||
/* Determines if a given theme is local / not a custom user stylesheet */
|
||||
isThemeLocal(themeToCheck) {
|
||||
const localThemes = [...Defaults.builtInThemes, ...this.extraThemeNames];
|
||||
return localThemes.includes(themeToCheck);
|
||||
},
|
||||
/* Opens the theme color configurator popup */
|
||||
openThemeConfigurator() {
|
||||
this.$store.commit(Keys.SET_MODAL_OPEN, true);
|
||||
@@ -154,24 +64,6 @@ export default {
|
||||
this.themeConfiguratorOpen = false;
|
||||
}
|
||||
},
|
||||
/* Updates theme. Checks if the new theme is local or external,
|
||||
and calls appropirate updating function. Updates local storage */
|
||||
updateTheme(newTheme) {
|
||||
if (newTheme === 'Default') {
|
||||
this.resetToDefault();
|
||||
this.themeHelper.theme = 'Default';
|
||||
} else if (this.isThemeLocal(newTheme)) {
|
||||
this.ApplyLocalTheme(newTheme);
|
||||
} else {
|
||||
this.themeHelper.theme = newTheme;
|
||||
}
|
||||
this.ApplyCustomVariables(newTheme);
|
||||
// localStorage.setItem(localStorageKeys.THEME, newTheme);
|
||||
},
|
||||
/* Removes any applied themes */
|
||||
resetToDefault() {
|
||||
document.getElementsByTagName('html')[0].removeAttribute('data-theme');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -74,6 +74,7 @@ export default {
|
||||
this.jokeLine2 = data.delivery;
|
||||
} else if (this.jokeType === 'single') {
|
||||
this.jokeLine1 = data.joke;
|
||||
this.jokeLine2 = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
}
|
||||
},
|
||||
processData(data) {
|
||||
this.data = data.data.sort((a, b) => a.vmid > b.vmid);
|
||||
this.data = data.data.sort((a, b) => Number(a.vmid) > Number(b.vmid));
|
||||
if (this.hideTemplates) {
|
||||
this.data = this.data.filter(item => item.template !== 1);
|
||||
}
|
||||
|
||||
242
src/components/Widgets/UptimeKuma.vue
Normal file
242
src/components/Widgets/UptimeKuma.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="monitors">
|
||||
<div v-for="(monitor, index) in monitors" :key="index" class="item-wrapper">
|
||||
<div class="item monitor-row">
|
||||
<div class="title-title"><span class="text">{{ monitor.name }}</span></div>
|
||||
<div class="monitors-container">
|
||||
<div class="status-container">
|
||||
<span class="status-pill" :class="[monitor.statusClass]">{{ monitor.status }}</span>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<span class="response-time">{{ monitor.responseTime }}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="errorMessage">
|
||||
<div class="error-message">
|
||||
<span class="text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* A simple example which you can use as a template for creating your own widget.
|
||||
* Takes two optional parameters (`text` and `count`), and fetches a list of images
|
||||
* from dummyapis.com, then renders the results to the UI.
|
||||
*/
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
monitors: null,
|
||||
errorMessage: null,
|
||||
errorMessageConstants: {
|
||||
missingApiKey: 'No API key set',
|
||||
missingUrl: 'No URL set',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
},
|
||||
computed: {
|
||||
/* Get API key for access to instance */
|
||||
apiKey() {
|
||||
const { apiKey } = this.options;
|
||||
|
||||
return apiKey;
|
||||
},
|
||||
/* Get instance URL */
|
||||
url() {
|
||||
const { url } = this.options;
|
||||
|
||||
return url;
|
||||
},
|
||||
/* Create authorisation header for the instance from the apiKey */
|
||||
authHeaders() {
|
||||
if (!this.options.apiKey) {
|
||||
return {};
|
||||
}
|
||||
const encoded = window.btoa(`:${this.options.apiKey}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* The update() method extends mixin, used to update the data.
|
||||
* It's called by parent component, when the user presses update
|
||||
*/
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.fetchData();
|
||||
},
|
||||
/* Make the data request to the computed API endpoint */
|
||||
fetchData() {
|
||||
const { authHeaders, url } = this;
|
||||
|
||||
if (!this.optionsValid({ authHeaders, url })) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.makeRequest(url, authHeaders)
|
||||
.then(this.processData);
|
||||
},
|
||||
/* Convert API response data into a format to be consumed by the UI */
|
||||
processData(response) {
|
||||
const monitorRows = this.getMonitorRows(response);
|
||||
|
||||
const monitors = new Map();
|
||||
|
||||
for (let index = 0; index < monitorRows.length; index += 1) {
|
||||
const row = monitorRows[index];
|
||||
this.processRow(row, monitors);
|
||||
}
|
||||
|
||||
this.monitors = Array.from(monitors.values());
|
||||
},
|
||||
getMonitorRows(response) {
|
||||
return response.split('\n').filter(row => row.startsWith('monitor_'));
|
||||
},
|
||||
processRow(row, monitors) {
|
||||
const dataType = this.getRowDataType(row);
|
||||
const monitorName = this.getRowMonitorName(row);
|
||||
|
||||
if (!monitors.has(monitorName)) {
|
||||
monitors.set(monitorName, { name: monitorName });
|
||||
}
|
||||
|
||||
const monitor = monitors.get(monitorName);
|
||||
const value = this.getRowValue(row);
|
||||
|
||||
const updated = this.setMonitorValue(dataType, monitor, value);
|
||||
|
||||
monitors.set(monitorName, updated);
|
||||
},
|
||||
setMonitorValue(key, monitor, value) {
|
||||
const copy = { ...monitor };
|
||||
switch (key) {
|
||||
case 'monitor_cert_days_remaining': {
|
||||
copy.certDaysRemaining = value;
|
||||
break;
|
||||
}
|
||||
case 'monitor_cert_is_valid': {
|
||||
copy.certValid = value;
|
||||
break;
|
||||
}
|
||||
case 'monitor_response_time': {
|
||||
copy.responseTime = value;
|
||||
break;
|
||||
}
|
||||
case 'monitor_status': {
|
||||
copy.status = value === '1' ? 'Up' : 'Down';
|
||||
copy.statusClass = copy.status.toLowerCase();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return copy;
|
||||
},
|
||||
getRowValue(row) {
|
||||
return this.getValueWithRegex(row, /\b\d+\b$/);
|
||||
},
|
||||
getRowMonitorName(row) {
|
||||
return this.getValueWithRegex(row, /monitor_name="([^"]+)"/);
|
||||
},
|
||||
getRowDataType(row) {
|
||||
return this.getValueWithRegex(row, /^(.*?)\{/);
|
||||
},
|
||||
getValueWithRegex(string, regex) {
|
||||
const result = string.match(regex);
|
||||
|
||||
const isArray = Array.isArray(result);
|
||||
|
||||
if (!isArray) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return result.length > 1 ? result[1] : result[0];
|
||||
},
|
||||
optionsValid({ url, authHeaders }) {
|
||||
const errors = [];
|
||||
if (url === undefined) {
|
||||
errors.push(this.errorMessageConstants.missingUrl);
|
||||
}
|
||||
|
||||
if (authHeaders === undefined) {
|
||||
errors.push(this.errorMessageConstants.missingApiKey);
|
||||
}
|
||||
|
||||
if (errors.length === 0) { return true; }
|
||||
|
||||
this.errorMessage = errors.join('\n');
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.status-pill {
|
||||
border-radius: 50em;
|
||||
box-sizing: border-box;
|
||||
font-size: 0.75em;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
padding: .35em .65em;
|
||||
margin: 1em 0.5em;
|
||||
min-width: 64px;
|
||||
|
||||
&.up {
|
||||
background-color: rgb(92, 221, 139);
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.down {
|
||||
background-color: rgb(220, 53, 69);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
div.item.monitor-row:hover {
|
||||
background-color: var(--item-background);
|
||||
color: var(--current-color);
|
||||
opacity: 1;
|
||||
|
||||
div.title-title>span.text {
|
||||
color: var(--current-color);
|
||||
}
|
||||
}
|
||||
|
||||
.monitors-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.monitor-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.35em 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -115,6 +115,7 @@ const COMPAT = {
|
||||
'synology-download': 'SynologyDownload',
|
||||
'system-info': 'SystemInfo',
|
||||
'tfl-status': 'TflStatus',
|
||||
'uptime-kuma': 'UptimeKuma',
|
||||
'wallet-balance': 'WalletBalance',
|
||||
weather: 'Weather',
|
||||
'weather-forecast': 'WeatherForecast',
|
||||
@@ -205,14 +206,16 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/media-queries.scss';
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.widget-base {
|
||||
position: relative;
|
||||
padding: 0.75rem 0.5rem 0.5rem 0.5rem;
|
||||
background: var(--widget-base-background);
|
||||
box-shadow: var(--widget-base-shadow, none);
|
||||
|
||||
// Refresh and full-page action buttons
|
||||
button.action-btn {
|
||||
button.action-btn {
|
||||
height: 1rem;
|
||||
min-width: auto;
|
||||
width: 1.75rem;
|
||||
@@ -223,21 +226,26 @@ export default {
|
||||
border: none;
|
||||
opacity: var(--dimming-factor);
|
||||
color: var(--widget-text-color);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--widget-background-color);
|
||||
}
|
||||
|
||||
&.update-btn {
|
||||
right: -0.25rem;
|
||||
}
|
||||
|
||||
&.open-btn {
|
||||
right: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional widget label
|
||||
.widget-label {
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
|
||||
// Actual widget container
|
||||
.widget-wrap {
|
||||
&.has-error {
|
||||
@@ -245,9 +253,11 @@ export default {
|
||||
opacity: 0.5;
|
||||
border-radius: var(--curve-factor);
|
||||
background: #ffff0040;
|
||||
|
||||
&:hover { background: none; }
|
||||
}
|
||||
}
|
||||
|
||||
// Error message output
|
||||
.widget-error {
|
||||
p.error-msg {
|
||||
@@ -256,12 +266,14 @@ export default {
|
||||
font-size: 1rem;
|
||||
margin: 0 auto 0.5rem auto;
|
||||
}
|
||||
|
||||
p.error-output {
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--widget-text-color);
|
||||
font-size: 0.85rem;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
|
||||
p.retry-link {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
@@ -270,14 +282,17 @@ export default {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading spinner
|
||||
.loading {
|
||||
margin: 0.2rem auto;
|
||||
text-align: center;
|
||||
|
||||
svg.loader {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide widget contents while loading
|
||||
&.is-loading {
|
||||
.widget-wrap {
|
||||
@@ -285,5 +300,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user