🔀 Merge branch 'master' of github.com:Lissy93/dashy into REFACTOR/widget-and-docs-improvments

This commit is contained in:
Alicia Sykes
2022-06-30 15:00:51 +01:00
30 changed files with 2174 additions and 626 deletions

View File

@@ -175,9 +175,13 @@ export default {
if (this.enableStatusCheck) this.checkWebsiteStatus();
// If continious status checking is enabled, then start ever-lasting loop
if (this.statusCheckInterval > 0) {
setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
this.intervalId = setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
}
},
beforeDestroy() {
// Stop periodic status-check when item is destroyed (e.g. navigating in multi-page setup)
if (this.intervalId) clearInterval(this.intervalId);
},
};
</script>

View File

@@ -1,15 +1,15 @@
<template>
<div class="apod-wrapper" v-if="image">
<div class="apod-wrapper" v-if="url">
<a :href="link" class="title" target="__blank" title="View Article">
{{ title }}
</a>
<a :href="hdImage" title="View HD Image" class="picture" target="__blank">
<img :src="image" :alt="title" />
<a :href="hdurl" title="View HD Image" class="picture" target="__blank">
<img :src="url" :alt="title" />
</a>
<p class="copyright">{{ copyright }}</p>
<p class="description">{{ truncatedDescription }}</p>
<p class="explanation">{{ truncatedExplanation }}</p>
<p @click="toggleShowFull" class="expend-details-btn">
{{ showFullDesc ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
{{ showFullExp ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
</p>
</div>
</template>
@@ -24,17 +24,17 @@ export default {
data() {
return {
title: null,
image: null,
hdImage: null,
link: null,
description: null,
url: null,
hdurl: null,
link: 'https://apod.nasa.gov/apod/astropix.html',
explanation: null,
copyright: null,
showFullDesc: false,
showFullExp: false,
};
},
computed: {
truncatedDescription() {
return this.showFullDesc ? this.description : `${this.description.substring(0, 100)}...`;
truncatedExplanation() {
return this.showFullExp ? this.explanation : `${this.explanation.substring(0, 100)}...`;
},
},
methods: {
@@ -52,14 +52,14 @@ export default {
},
processData(data) {
this.title = data.title;
this.image = data.url;
this.hdImage = data.hdurl;
this.link = data.apod_site;
this.description = data.description;
this.url = data.url;
this.hdurl = data.hdurl;
this.link = data.link;
this.explanation = data.explanation;
this.copyright = data.copyright;
},
toggleShowFull() {
this.showFullDesc = !this.showFullDesc;
this.showFullExp = !this.showFullExp;
},
},
};
@@ -85,7 +85,7 @@ export default {
opacity: var(--dimming-factor);
color: var(--widget-text-color);
}
p.description {
p.explanation {
color: var(--widget-text-color);
font-size: 1rem;
margin: 0.5rem 0;

View File

@@ -0,0 +1,208 @@
<template>
<div class="nextcloud-widget nextcloud-status-wrapper">
<div v-if="notifications.length">
<!-- group actions: delete all -->
<p v-if="canDeleteNotification('delete-all')" class="group-action">
<span class="action secondary" @click="deleteNotifications">{{ tt('delete-all') }}</span>
</p>
<hr/>
<!-- notifications list -->
<div v-for="(notification, idx) in notifications" :key="idx" class="notification">
<div><img :src="notificationIcon(notification.icon)" /></div>
<div>
<p>
<small class="date" v-tooltip="dateTooltip(notification)">
{{ getTimeAgo(Date.parse(notification.datetime)) }}
</small> <span v-tooltip="subjectTooltip(notification)">{{ notification.subject }} </span>
<!-- notifications item: action links -->
<span v-if="notification.actions.length">
<span v-for="(action, idx) in notification.actions" :key="idx">
<a :href="action.link" class="action" target="_blank">{{ action.label }}</a>
</span>
</span>
<span v-if="canDeleteNotification('delete')">
<a @click="deleteNotification(notification.notification_id)"
class="action secondary">{{ tt('delete-notification') }}</a>
</span>
</p>
</div>
<hr/>
</div>
</div>
<!-- empty notifications list -->
<div v-else class="sep">
<p>{{ tt('no-notifications') }}</p>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
/**
* NextcloudNotifications widget - Displays the user's notifications
* Used endpoints
* - capabilities: to determine if the User Notification API is enabled
* - notifications: to fetch list of notifications, delete all or a single notification
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
data() {
return {
notifications: [],
};
},
computed: {
/* Parse the limit user option to either an integer or to an integer + 'm', 'h' or 'd' */
limit() {
const lim = this.options.limit;
const defaultLimit = [0, false];
if (typeof lim === 'string') {
const k = { m: 60, h: 60 * 60, d: 60 * 60 * 24 };
const m = lim.match(/(\d+)([hmd])/);
if (m.length !== 3) return defaultLimit;
return [false, m[1] * k[m[2]] * 1000];
}
if (typeof lim === 'number') {
return [parseInt(this.options.limit, 10) || 0, false];
}
return defaultLimit;
},
},
methods: {
allowedStatuscodes() {
return [100, 200];
},
async fetchData() {
if (!this.hasValidCredentials()) return;
await this.loadCapabilities();
if (!this.capabilities?.notifications?.enabled) {
this.error('This Nextcloud server doesn\'t support the Notifications API');
return;
}
this.makeRequest(this.endpoint('notifications'), this.headers)
.then(this.processNotifications)
.finally(this.finishLoading);
},
processNotifications(response) {
const notifications = this.validateResponse(response);
const [limitCount, limitTime] = this.limit;
this.notifications = [];
notifications.forEach((notification) => {
if (limitCount && this.notifications.length === limitCount) return; // count limit
const notiTime = Date.parse(notification.datetime);
const nowTime = new Date().getTime();
if (limitTime && notiTime && nowTime - notiTime > limitTime) return; // time limit
this.notifications.push(notification);
});
},
/* Transform icon URL to SVG Color API request URL
* @see https://docs.nextcloud.com/server/latest/developer_manual/html_css_design/icons.html */
notificationIcon(url) {
const color = this.getValueFromCss('widget-text-color').replace('#', '');
return url.replace('core/img', 'svg/core')
.replace(/extra-apps\/([^/]+)\/img/, 'svg/$1')
.replace(/apps\/([^/]+)\/img/, 'svg/$1')
.replace('.svg', `?color=${color}`);
},
/* Notification actions */
canDeleteNotification(deleteTarget) {
const capNotif = this.capabilities?.notifications?.features;
return Array.isArray(capNotif) && capNotif.includes(deleteTarget);
},
deleteNotifications() {
this.makeRequest(this.endpoint('notifications'), this.headers, 'DELETE')
.then(() => {
this.notifications = [];
});
},
deleteNotification(id) {
this.makeRequest(`${this.endpoint('notifications')}/${id}`, this.headers, 'DELETE')
.then(this.fetchData);
},
/* Tooltip generators */
subjectTooltip(notification) {
const content = notification.message;
return {
content, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
dateTooltip(notification) {
const content = new Date(Date.parse(notification.datetime)).toLocaleString();
return {
content, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 60;
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-status-wrapper {
div p small i {
position: relative;
top: .25em;
}
small.date {
background: var(--widget-text-color);
color: var(--widget-accent-color);
border-radius: .25em;
padding: .15em .3em;
margin: .25em .25em .25em 0;
display: inline-block;
font-weight: bold;
}
p.group-action {
margin-top: 0;
}
span.action, span a.action {
cursor: pointer;
margin: .1em .5em .1em 0;
padding: .15em;
border-radius: .25em;
white-space: nowrap;
}
span.action:hover, span a.action:hover {
background: var(--widget-text-color);
color: var(--widget-accent-color);
text-decoration: underline;
}
.secondary {
opacity: .5;
font-size: 75%;
margin-left: .2rem;
}
div.notification {
display: table;
width: 100%;
> div:first-child {
float: right;
}
> div:nth-child(2) {
float: left;
width: 93%;
}
> div {
display: table-cell;
text-align: left;
> img {
float: right;
width: 1em;
position: relative;
top: 1em;
opacity: .75;
}
}
}
div hr {
margin-top: .3em;
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,214 @@
<template>
<div v-if="didLoadData" class="nextcloud-widget nextcloud-phpopcache-wrapper">
<div class="sep">
<!-- PHP opcache enabled and cache full -->
<p v-tooltip="opcacheStartTimeTooltip()">
<i class="fal fa-microchip"></i>
<strong>PHP opcache</strong>&nbsp;
<em v-if="opcache.opcache_enabled" class="oc-enabled">
{{ tt('enabled') }}
</em>
<em v-else class="oc-disabled">{{ tt('disabled') }}</em>&nbsp;
<strong v-if="opcache.cache_full" class="oc-full">
<i class="far fa-siren-on"></i>{{ tt('cache-full') }}
</strong>
</p>
<hr/>
<!-- PHP opcache stats -->
<div v-if="opcache.opcache_enabled">
<!-- PHP opcache stats: hit/miss -->
<p v-tooltip="opcacheStatsTooltip()">
<i class="fal fa-bullseye-arrow"></i>
<em v-html="formatNumber(opcache_stats.hits)"></em>&nbsp;
<small>{{ tt('hits') }}</small>&nbsp;
<em v-html="formatNumber(opcache_stats.misses)"></em>&nbsp;
<small>{{ tt('misses') }}</small>&nbsp;
<em v-html="formatPercent(opcache_stats.opcache_hit_rate, 3)"></em>&nbsp;
<small>{{ tt('hit-rate') }}</small>
</p>
<hr/>
<!-- PHP opcache stats: memory -->
<p v-tooltip="opcacheMemoryUsageTooltip()">
<i class="fal fa-memory"></i>
<em v-html="formatPercent(opcache.memory_usage.used_memory_percentage, 1)"></em>&nbsp;
<small>of</small>
<em v-html="convertBytes(opcache.memory_usage.total_memory)"></em>&nbsp;
<small>{{ tt('memory-used') }}</small>
</p>
<hr/>
<!-- PHP opcache stats: interned strings -->
<p v-tooltip="opcacheInternedStringsTooltip()">
<i class="fal fa-puzzle-piece"></i>
<em v-html="formatNumber(opcache.interned_strings_usage.number_of_strings, 1, true)"></em>
&nbsp;<small>{{ tt('strings-use') }}</small>
<em v-html="formatPercent(opcache.interned_strings_usage.used_memory_percentage)"></em>
&nbsp;<small>{{ tt('of') }}</small>
<em v-html="convertBytes(opcache.interned_strings_usage.total_memory)"></em>
</p>
<hr/>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
/**
* NextcloudPhpOpcache widget - Shows statistics about PHP opcache performance
* Used endpoints
* - serverinfo: requires Nextcloud admin user
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
data() {
return {
opcache: {
opcache_enabled: null,
full: null,
opcache_statistics: {
num_cached_scripts: null,
num_cached_keys: null,
max_cached_keys: null,
hits: null,
start_time: null,
last_restart_time: null,
misses: null,
opcache_hit_rate: null,
},
memory_usage: {
used_memory: null,
free_memory: null,
total_memory: null,
wasted_memory: null,
used_memory_percentage: null,
current_wasted_percentage: null,
},
interned_strings_usage: {
buffer_size: null,
used_memory: null,
total_memory: null,
free_memory: null,
number_of_strings: null,
used_memory_percentage: null,
},
},
};
},
computed: {
didLoadData() {
return typeof (this?.opcache?.opcache_enabled) === 'boolean';
},
// shortcuts to data members
opcache_stats() {
return this.opcache.opcache_statistics;
},
opcache_interned() {
return this.opcache.interned_strings_usage;
},
},
methods: {
allowedStatuscodes() {
return [200];
},
fetchData() {
if (!this.hasValidCredentials()) return;
this.makeRequest(this.endpoint('serverinfo'), this.headers)
.then(this.processServerInfo)
.finally(() => this.finishLoading());
},
processServerInfo(serverData) {
const data = this.validateResponse(serverData);
this.opcache = data.server?.php?.opcache;
if (!this.opcache) return;
this.updateOpcacheMemory();
this.updateOpcacheInterned();
},
updateOpcacheMemory() {
this.opcache_stats.opcache_hit_rate = parseFloat(
this.opcache_stats.opcache_hit_rate,
).toFixed(3);
this.opcache.memory_usage.total_memory = (
this.opcache.memory_usage.used_memory + this.opcache.memory_usage.free_memory
);
this.opcache.memory_usage.used_memory_percentage = parseFloat(
(this.opcache.memory_usage.used_memory / this.opcache.memory_usage.total_memory) * 100,
).toFixed(1);
},
updateOpcacheInterned() {
this.opcache_interned.total_memory = (
this.opcache_interned.used_memory + this.opcache_interned.free_memory
);
this.opcache_interned.used_memory_percentage = parseFloat(
(this.opcache_interned.used_memory / this.opcache_interned.total_memory) * 100,
).toFixed(5);
},
/* Tooltip generators */
opcacheStartTimeTooltip() {
let content = `${this.tt('started')} `
+ `${new Date(this.opcache_stats.start_time * 1000).toLocaleString()}`;
if (this.opcache_stats.last_restart_time) {
content = content.concat(
`<br><br>${this.tt('last-restart')} `
+ `${new Date(this.opcache_stats.last_restart_time * 1000).toLocaleString()}`,
);
}
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
opcacheStatsTooltip() {
const content = `${parseFloat(this.opcache_stats.hits).toLocaleString()} ${this.tt('hits')}<br>`
+ `${parseFloat(this.opcache_stats.misses).toLocaleString()} ${this.tt('misses')}<br><br>`
+ `${parseFloat(this.opcache_stats.num_cached_scripts).toLocaleString()} ${this.tt('scripts')}<br>`
+ `${parseFloat(this.opcache_stats.num_cached_keys).toLocaleString()} ${this.tt('keys')}<br>`
+ `${parseFloat(this.opcache_stats.max_cached_keys).toLocaleString()} ${this.tt('max-keys')}<br>`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
opcacheMemoryUsageTooltip() {
const content = `PHP opcache ${this.tt('memory-utilisation')}<br><br>`
+ `${this.convertBytes(this.opcache.memory_usage.total_memory)} ${this.tt('total')}<br>`
+ `${this.convertBytes(this.opcache.memory_usage.used_memory)} ${this.tt('used')}<br>`
+ `${this.convertBytes(this.opcache.memory_usage.free_memory)} ${this.tt('free')}<br><br>`
+ `${this.convertBytes(this.opcache.memory_usage.wasted_memory)} (`
+ `${parseFloat(this.opcache.memory_usage.current_wasted_percentage).toFixed(1)}%) ${this.tt('wasted')}`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
opcacheInternedStringsTooltip() {
const content = 'PHP opcache interned strings<br><br>'
+ `${this.convertBytes(this.opcache_interned.buffer_size)} ${this.tt('total')} ${this.tt('memory')}<br>`
+ `${this.convertBytes(this.opcache_interned.used_memory)} ${this.tt('used')} ${this.tt('memory')}<br>`
+ `${this.convertBytes(this.opcache_interned.free_memory)} ${this.tt('free')} ${this.tt('memory')}<br><br>`
+ `${parseFloat(this.opcache_interned.number_of_strings).toLocaleString()}`
+ ' strings';
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 60;
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-phpopcache-wrapper {
.oc-enabled {
color: var(--success);
}
.oc-disabled {
color: var(--neutral);
}
.oc-full {
color: var(--danger);
}
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div v-if="didLoadData" class="nextcloud-widget nextcloud-stats-wrapper">
<div class="server-info sep">
<!-- server info: users -->
<div v-if="activeUsers">
<p v-tooltip="activeUsersTooltip()">
<i class="fal fa-user"></i>
<em v-html="formatNumber(storage.num_users)"></em>
<strong>{{ tt('total-users') }}</strong> <small>{{ tt('of-which') }}</small>
<em v-html="formatNumber(activeUsers.last24hours)"></em>
<strong>{{ tt('active') }}</strong> <small>({{ tt('last-24-hours') }})</small>
</p>
</div>
<hr />
<div v-if="nextcloud">
<!-- server info: apps -->
<p v-tooltip="appUpdatesTooltip()">
<i class="fal fa-browser"></i>
<em v-html="formatNumber(apps.num_installed)"></em>
<strong>{{ tt('applications') }}</strong>
<span v-if="apps.num_updates_available" class="nc-updates">
<i class="fal fa-download"></i><em>{{ apps.num_updates_available }}</em>
<strong>
{{ tt('updates-available',
{plural: apps.num_updates_available > 1 ? 's' : ''}) }}
</strong>
</span>
<small v-else data-nc-updates class="disabled">{{ tt('no-pending-updates') }}</small>
</p>
<hr />
<!-- server info: storage -->
<p v-tooltip="storagesTooltip()">
<i class="fal fa-file"></i><em v-html="formatNumber(storage.num_files)"></em>
<strong>{{ tt('files', { plural: storage.num_files > 1 ? 's' : '' }) }}</strong>&nbsp;
<small>{{ tt('in') }}</small><em>{{ storage.num_storages }}</em>
<strong>{{ tt('storages', { plural: storage.num_storages > 1 ? 's' : '' }) }}</strong>
&nbsp;&nbsp;<strong v-html="convertBytes(system.freespace)"></strong>&nbsp;
<small>{{ tt('free') }}</small>
</p>
<hr />
<!-- server info: shares -->
<p v-tooltip="sharesTooltip()">
<i class="fal fa-share"></i>
<em v-html="formatNumber(shares.num_shares)"></em>
<strong>{{ tt('local') }}</strong> <small> {{ tt('and') }}</small>
<em v-html="formatNumber(shares.num_fed_shares_sent
+ shares.num_fed_shares_received)"></em>
<strong>
{{ tt('federated-shares') }}
</strong>
</p>
<hr />
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
/**
* NextcloudStats widget - Shows statistics about Nextcloud usage
* Used endpoints
* - serverinfo: requires Nextcloud admin user
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
data() {
return {
nextcloud: {
system: {
freespace: null,
apps: {
num_installed: null,
num_updates_available: 0,
app_updates: [],
},
},
storage: {
num_users: null,
num_files: null,
num_storages: null,
},
shares: {
num_shares: null,
num_shares_user: null,
num_shares_groups: null,
num_shares_link: null,
num_shares_mail: null,
num_shares_room: null,
num_shares_link_no_password: null,
num_fed_shares_sent: null,
num_fed_shares_received: null,
},
},
activeUsers: {
last5minutes: null,
last1hour: null,
last24hours: null,
},
};
},
computed: {
didLoadData() {
return !!(this?.system?.freespace);
},
// data shortcuts
system() {
return this.nextcloud.system;
},
storage() {
return this.nextcloud.storage;
},
shares() {
return this.nextcloud.shares;
},
apps() {
return this.nextcloud.system.apps;
},
},
methods: {
allowedStatuscodes() {
return [200];
},
fetchData() {
if (!this.hasValidCredentials()) return;
this.makeRequest(this.endpoint('serverinfo'), this.headers)
.then(this.processServerInfo)
.finally(this.finishLoading);
},
processServerInfo(serverResponse) {
const data = this.validateResponse(serverResponse);
this.nextcloud = data.nextcloud;
this.activeUsers = data.activeUsers;
},
/* Tooltip generators */
activeUsersTooltip() {
const content = `${parseFloat(this.activeUsers.last5minutes).toLocaleString()}`
+ ` ${this.tt('last-5-minutes')}<br>`
+ `${parseFloat(this.activeUsers.last1hour).toLocaleString()}`
+ ` ${this.tt('last-hour')}<br>`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
appUpdatesTooltip() {
let content = `<strong>${this.tt('updates-available-for')}</strong><ul>`;
Object.entries(this.system.apps.app_updates).forEach(([app, version]) => {
content += `<li>${app}: ${version}</li>`;
});
content += '</ul>';
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
storagesTooltip() {
const content = `<strong>${this.tt('storages-by-type')}</strong><ul><li>`
+ `${parseFloat(this.storage.num_storages_local).toLocaleString()} ${this.tt('local')}</li><li>`
+ `${parseFloat(this.storage.num_storages_home).toLocaleString()} ${this.tt('home')}</li><li>`
+ `${parseFloat(this.storage.num_storages_other).toLocaleString()} ${this.tt('other')}</li></ul>`
+ `${parseFloat(this.storage.num_files).toLocaleString()} ${this.tt('total-files')}`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
sharesTooltip() {
const content = `<strong>${this.tt('local-shares')}</strong><ul><li>`
+ `${parseFloat(this.shares.num_shares_user).toLocaleString()} ${this.tt('user')}</li><li>`
+ `${parseFloat(this.shares.num_shares_groups).toLocaleString()} ${this.tt('groups')}</li><li>`
+ `${parseFloat(this.shares.num_shares_mail).toLocaleString()} ${this.tt('email')}</li><li>`
+ `${parseFloat(this.shares.num_shares_room).toLocaleString()} ${this.tt('chat-room')}</li><li>`
+ `${parseFloat(this.shares.num_shares_link).toLocaleString()} ${this.tt('private-link')}</li><li>`
+ `${parseFloat(this.shares.num_shares_link_no_password).toLocaleString()} ${this.tt('public-link')}</li></ul>`
+ `<strong>${this.tt('federated-shares-ucfirst')}</strong><ul><li>`
+ `${parseFloat(this.shares.num_fed_shares_sent).toLocaleString()} ${this.tt('sent')}</li><li>`
+ `${parseFloat(this.shares.num_fed_shares_received).toLocaleString()} ${this.tt('received')}</li></ul>`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 20;
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-stats-wrapper {
div.server-info .nc-updates {
color: var(--success);
margin-left: .5em;
}
}
</style>

View File

@@ -0,0 +1,230 @@
<template>
<div v-if="didLoadData" class="nextcloud-widget nextcloud-system-wrapper">
<div class="charts">
<!-- memory gauge -->
<div class="chart-container">
<small>{{ tt('overall') }} {{ tt('memory-utilisation') }}</small>
<GaugeChart :value="memoryGauge.value"
:baseColor="memoryGauge.background"
:gaugeColor="memoryGauge.color">
<p class="percentage">{{ memoryGauge.value }}%</p>
</GaugeChart>
<small>{{ getMemoryGaugeLabel() }}</small>
</div>
<!-- cpu load chart -->
<div>
<div
:id="cpuLoadChartId" class="load-chart"
v-tooltip="$t('widgets.glances.system-load-desc')"></div>
</div>
</div>
<div>
<!-- server info: server -->
<hr />
<p>
<i class="fal fa-server"></i>
<strong>Nextcloud</strong>
<em>{{ server.nextcloud.system.version }}</em> <small> </small>
<strong>{{ server.server.webserver }}/PHP</strong>
<em>{{ server.server.php.version }}</em>
</p>
<hr />
<!-- server info: database -->
<p>
<i class="fal fa-database"></i>
<strong>{{ server.server.database.type }}</strong>
<em>{{ server.server.database.version }}</em> <small>{{ tt('using') }}</small>
<em v-html="convertBytes(server.server.database.size)"></em>
</p>
<hr/>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
import GaugeChart from '@/components/Charts/Gauge';
import ChartingMixin from '@/mixins/ChartingMixin';
/**
* NextcloudSystem widget - Visualises CPU load and memory utilisation and shows server versions
* Used endpoints
* - serverinfo: requires Nextcloud admin user
*/
export default {
mixins: [WidgetMixin, NextcloudMixin, ChartingMixin],
components: { GaugeChart },
data() {
return {
server: {
server: {
database: {
type: null,
version: null,
size: null,
},
webserver: null,
php: {
version: null,
},
},
nextcloud: {
system: {
version: null,
freespace: null,
cpuload: [],
mem_total: null,
mem_free: null,
mem_percent: null,
},
},
},
memoryGauge: {
value: 0,
color: '#272f4d',
showMoreInfo: false,
moreInfo: null,
background: '#16161d',
},
};
},
computed: {
cpuLoadChartId() {
return `nextcloud-cpu-load-chart-${Math.random().toString().slice(-4)}`;
},
didLoadData() {
return !!(this.server?.nextcloud?.system?.version);
},
},
methods: {
allowedStatuscodes() {
return [200];
},
async fetchData() {
if (!this.hasValidCredentials()) return;
this.makeRequest(this.endpoint('serverinfo'), this.headers)
.then(this.processServerInfo)
.finally(() => this.finishLoading());
},
processServerInfo(serverData) {
const data = this.validateResponse(serverData);
if (!data || data.length === 0) return;
this.server.nextcloud.system = data.nextcloud?.system;
this.server.server.php.version = data.server?.php?.version;
this.server.server.database = data.server?.database;
this.server.server.webserver = data.server?.webserver;
},
updateMemoryGauge(sys) {
this.memoryGauge.value = parseFloat(
(((sys.mem_total - sys.mem_free) / sys.mem_total) * 100).toFixed(2),
);
this.memoryGauge.color = this.getMemoryGaugeColor(this.memoryGauge.value);
},
updateOpcacheMemory() {
this.opcache_stats.opcache_hit_rate = parseFloat(
this.opcache_stats.opcache_hit_rate,
).toFixed(3);
this.opcache.memory_usage.total_memory = (
this.opcache.memory_usage.used_memory + this.opcache.memory_usage.free_memory
);
this.opcache.memory_usage.used_memory_percentage = parseFloat(
(this.opcache.memory_usage.used_memory / this.opcache.memory_usage.total_memory) * 100,
).toFixed(1);
},
updateOpcacheInterned() {
this.opcache.interned_strings_usage.total_memory = (
this.opcache.interned_strings_usage.used_memory
+ this.opcache.interned_strings_usage.free_memory
);
this.opcache.interned_strings_usage.used_memory_percentage = parseFloat(
(this.opcache.interned_strings_usage.used_memory
/ this.opcache.interned_strings_usage.total_memory) * 100,
).toFixed(5);
},
getMemoryGaugeColor(memPercent) {
if (memPercent < 50) return this.getColorRgba('widget-text-color', 0.6);
if (memPercent < 60) return this.getColorRgba('warning', 0.75);
if (memPercent < 80) return this.getColorRgba('error', 0.9);
if (memPercent < 100) return this.getColorRgba('danger');
return this.getColorRgba('background');
},
getMemoryGaugeLabel() {
const sys = this.server.nextcloud.system;
return `${this.convertBytes((sys.mem_total - sys.mem_free) * 1024, 2, false)} / `
+ `${this.convertBytes(sys.mem_total * 1024, 2, false)}`;
},
updateCpuLoad(load) {
const chartData = {
labels: ['1m', '5m', '15m'],
datasets: [{ values: [load[0], load[1], load[2]] }],
};
const chartTitle = this.tt('load-averages');
this.renderCpuLoadChart(chartData, chartTitle);
},
renderCpuLoadChart(loadBarChartData, chartTitle) {
return new this.Chart(`#${this.cpuLoadChartId}`, {
title: chartTitle,
data: loadBarChartData,
type: 'bar',
height: 180,
colors: [this.getColorRgba('widget-text-color', 0.6)],
barOptions: {
spaceRatio: 0.2,
},
tooltipOptions: {
formatTooltipY: d => `${d} ${this.tt('tasks')}`,
},
});
},
},
created() {
this.overrideUpdateInterval = 30;
},
updated() {
const load = this.server?.nextcloud?.system?.cpuload;
if (load) this.updateCpuLoad(load);
const sys = this.server.nextcloud.system;
if (sys) this.updateMemoryGauge(sys);
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-system-wrapper {
div.charts {
> div {
float: left;
}
> div:first-child {
max-width: 44%;
small {
font-size: 12px;
color: #666666;
display: inline-block;
width: 100%;
text-align: center;
margin: .9em 0 1.4em 0;
opacity: 1;
}
small:last-child {
margin-top: 2em;
font-size: 10px;
}
}
> div:nth-child(2) {
min-width: 55%;
}
p.percentage {
color: var(--widget-text-color);
text-align: center;
position: absolute;
font-size: 1.3em;
margin: .5em 0;
width: 100%;
bottom: 0;
}
}
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<div v-if="didLoadData" class="nextcloud-widget nextcloud-info-wrapper">
<!-- logo, branding, user info -->
<div>
<div class="logo">
<a :href="branding.url" target="_blank">
<img :src="branding.logo" />
</a>
<p>{{ branding.slogan }}</p>
</div>
<div class="info">
<p class="brand">{{ branding.name }}</p>
<p class="version" v-if="version.string">
<small>Nextcloud {{ tt('version') }} {{ version.string }}</small>
</p>
<p class="username">{{ user.displayName }} <em>({{ user.id }})</em></p>
<p class="login" v-tooltip="lastLoginTooltip()">
<span>{{ tt('last-login') }}</span>&nbsp;
<small>{{ getTimeAgo(user.lastLogin) }}</small>
</p>
</div>
</div>
<!-- disk space/quota -->
<div>
<div v-tooltip="quotaTooltip()">
<hr/>
<p>
<i class="fal fa-disc-drive"></i>
<strong v-if="user.quota.quota > 0">{{ tt('disk-quota') }}</strong>
<strong v-else>{{ tt('disk-space') }}</strong>
<em v-html="formatPercent(user.quota.relative)"></em>
<small>{{ tt('of') }}</small><strong v-html="convertBytes(user.quota.total)"></strong>
<span v-if="quotaChart.dataLoaded">
<PercentageChart :values="quotaChart.data" :showLegend="false" />
</span>
</p>
<hr/>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
import PercentageChart from '@/components/Charts/PercentageChart';
/**
* NextcloudUser widget - Displays branding and user information
* Used endpoints
* - capabilities: this delivers branding info (server name, logo, slogan, etc.)
* - user: name, last login, disk quota info
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: { PercentageChart },
data() {
return {
user: {
id: null,
displayName: null,
email: null,
quota: {
relative: null,
total: null,
used: null,
free: null,
quota: null,
},
},
quotaChart: {
dataLoaded: false,
data: [
{ label: null, size: null, color: null },
{ label: null, size: null, color: null },
],
},
};
},
computed: {
didLoadData() {
return !!this.user.id;
},
},
methods: {
allowedStatuscodes() {
return [100, 200];
},
fetchData() {
if (!this.hasValidCredentials()) return;
this.loadCapabilities()
.then(this.loadUser)
.finally(this.finishLoading);
},
loadUser() {
return this.makeRequest(this.endpoint('user'), this.headers)
.then(this.processUser);
},
processUser(userResponse) {
const user = this.validateResponse(userResponse);
this.user.id = user.id;
this.user.email = user.email;
this.user.quota = user.quota;
this.user.displayName = user.displayname;
this.user.lastLogin = user.lastLogin;
},
getQuotaChartColorUsed(percent) {
if (percent < 0.75) return this.getValueFromCss('widget-text-color');
if (percent < 0.85) return this.getValueFromCss('warning');
if (percent < 0.95) return this.getValueFromCss('error');
return this.getValueFromCss('danger');
},
updateQuotaChart() {
const used = parseFloat(this.user.quota.used / this.user.quota.total);
const free = parseFloat(this.user.quota.free / this.user.quota.total);
const d = this.quotaChart.data;
d[0] = { label: this.tt('used'), size: used, color: this.getQuotaChartColorUsed(used) };
d[1] = { label: this.tt('available'), size: free, color: this.getValueFromCss('success') };
this.quotaChart.dataLoaded = true;
},
/* Tooltip generators */
quotaTooltip() {
const quotaEnabled = this.user.quota.quota > 0;
const content = `${this.tt('quota-enabled', { not: quotaEnabled ? '' : 'not ' })}`
+ `<br><br>${this.convertBytes(this.user.quota.used)} ${this.tt('used')}<br>`
+ `${this.convertBytes(this.user.quota.free)} ${this.tt('free')}<br>`
+ `${this.convertBytes(this.user.quota.total)} ${this.tt('total')}`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
lastLoginTooltip() {
const content = new Date(this.user.lastLogin).toLocaleString();
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 120;
},
updated() {
this.updateQuotaChart();
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-info-wrapper {
> div:first-child {
display: flex;
}
> div:nth-child(2) {
border-top: none;
}
div.percentage-chart-wrapper {
margin: 0 .75em;
width: 5em;
position: relative;
top: .2em;
float: right;
}
div.logo {
width: 40%;
text-align: center;
img {
width: 8em;
}
p {
font-size: .9em;
opacity: .85;
}
}
div.info {
width: 56%;
p {
margin: 0 0 1rem 0;
}
p:last-child {
margin: 0;
}
p.brand {
margin: 0;
font-size: 1.35em;
font-weight: 800;
letter-spacing: 3px;
}
p.version small {
font-size: .75em;
}
p.username {
font-size: 1.1em;
em {
font-size: .9em;
}
}
p.login {
span {
font-size: .9em;
margin-right: .25em;
}
}
}
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<div class="nextcloud-widget nextcloud-user-status-wrapper">
<div v-if="didLoadData" class="sep">
<!-- user statuses: list -->
<div v-for="(status, userId) in statuses" :key="userId" class="user">
<div>
<!-- user status: emoji -->
<div>
<i>{{ status.icon }}</i>
</div>
<!-- user status: message -->
<div>
<p v-tooltip="clearAtTooltip(status.clearAt)">
<strong>{{ status.userId }}</strong>&nbsp;
<small v-if="status.clearAt"><i class="fal fa-clock"></i></small>
<span v-else-if="status.message"></span><em>{{ status.message }}</em>
</p>
</div>
<!-- user status: status -->
<div>
<p>
<small :class="`status ${status.status}`">
<i v-if="status.status === 'online' || status.status === 'dnd'"
class="fas fa-circle" v-tooltip="tt(status.status)"></i>
<i v-else class="far fa-circle" v-tooltip="tt(status.status)"></i>
</small>
</p>
</div>
</div>
<hr/>
</div>
</div>
<!-- user statuses: no content -->
<div v-else class="sep"><p>{{ tt('nothing-to-show') }}</p></div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';
// Nextcloud User Status API supports getting all user statuses at once
// or a single user's status. {fetchStrategy} determines which of these methods to use.
const fetchStrategies = {
allAtOnce: 'AllAtOnce',
oneByOne: 'OneByOne',
};
/**
* NextcloudUserStatus widget - Displays user statuses
* Used endpoints
* - capabilities: to determine if the User Status API is enabled
* - userstatus: to fetch a single or all user statuses
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
computed: {
didLoadData() {
return !!Object.keys(this?.statuses || {}).length;
},
fetchStrategy() {
if (!this.options.fetchStrategy) {
return fetchStrategies.allAtOnce;
}
if (!Object.values(fetchStrategies).includes(this.options.fetchStrategy)) {
return fetchStrategies.allAtOnce;
}
return this.options.fetchStrategy;
},
users() {
if (!this.options.users || !Array.isArray(this.options.users)) return [];
if (this.options.users.length > 100) return this.options.users.slice(0, 100);
return this.options.users;
},
showEmpty() {
return !!this.options.showEmpty;
},
},
data() {
return {
statuses: {},
};
},
methods: {
allowedStatuscodes() {
return [100, 200];
},
async fetchData() {
if (!this.hasValidCredentials() || !this.users.length) return;
await this.loadCapabilities();
if (!this.capabilities?.userStatus) {
this.error('This Nextcloud server doesn\'t support the User Status API');
return;
}
if (this.fetchStrategy === fetchStrategies.allAtOnce) {
this.makeRequest(this.endpoint('userstatus'), this.headers)
.then(this.processStatuses)
.finally(this.finishLoading);
} else {
const promises = [];
this.newStatuses = {};
this.users.forEach((user) => {
promises.push(
this.makeRequest(`${this.endpoint('userstatus')}/${user}`, this.headers)
.then(this.processStatus),
);
});
Promise.all(promises)
.then(() => {
this.statuses = this.newStatuses;
delete this.newStatuses;
})
.finally(this.finishLoading);
}
},
processStatuses(response) {
const statuses = this.validateResponse(response);
const newStatuses = {};
Object.values(statuses).forEach((status) => {
if (!this.users.includes(status.userId)) return;
if (!status.message && !this.showEmpty) return;
newStatuses[status.userId] = status;
});
this.statuses = newStatuses;
},
processStatus(response) {
const raw = this.validateResponse(response);
const status = Array.isArray(raw) && raw.length ? raw[0] : raw;
if (status && (status.message || this.showEmpty)) {
this.newStatuses[status.userId] = status;
}
},
/* Tooltip generators */
clearAtTooltip(clearAtTime) {
const content = clearAtTime ? `${this.tt('until')}`
+ ` ${new Date(clearAtTime * 1000).toLocaleString()}` : '';
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 60;
},
};
</script>
<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-user-status-wrapper {
.status {
float: right;
i {
position: relative;
top: .15rem;
margin: 0;
}
}
.online {
color: var(--success);
}
.offline {
color: var(--medium-grey);
}
.away {
color: var(--error);
}
.dnd {
color: var(--danger);
}
div.user > div {
display: table;
width: 100%;
> div:first-child {
width: 1.75em;
text-align: center;
> i {
font-style: normal;
}
}
> div:nth-child(2) {
p small i {
top: 0;
opacity: .5;
margin: 0;
}
}
> div {
display: table-cell;
text-align: left;
}
}
div.user hr {
margin-top: .3em;
margin-bottom: .3em;
}
div.user > div > div:last-child hr {
margin-bottom: 0;
}
}
</style>

View File

@@ -20,421 +20,13 @@
</div>
<!-- Widget -->
<div :class="`widget-wrap ${ error ? 'has-error' : '' }`">
<AdGuardDnsInfo
v-if="widgetType === 'adguard-dns-info'"
<component
v-bind:is="component"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<AdGuardFilterStatus
v-else-if="widgetType === 'adguard-filter-status'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<AdGuardStats
v-else-if="widgetType === 'adguard-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<AdGuardTopDomains
v-else-if="widgetType === 'adguard-top-domains'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<AnonAddy
v-else-if="widgetType === 'anonaddy'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Apod
v-else-if="widgetType === 'apod'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<BlacklistCheck
v-else-if="widgetType === 'blacklist-check'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Clock
v-else-if="widgetType === 'clock'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CryptoPriceChart
v-else-if="widgetType === 'crypto-price-chart'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CryptoWatchList
v-else-if="widgetType === 'crypto-watch-list'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CveVulnerabilities
v-else-if="widgetType === 'cve-vulnerabilities'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<DomainMonitor
v-else-if="widgetType === 'domain-monitor'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CodeStats
v-else-if="widgetType === 'code-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CovidStats
v-else-if="widgetType === 'covid-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<EmbedWidget
v-else-if="widgetType === 'embed'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<EthGasPrices
v-else-if="widgetType === 'eth-gas-prices'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<ExchangeRates
v-else-if="widgetType === 'exchange-rates'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Flights
v-else-if="widgetType === 'flight-data'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GitHubProfile
v-else-if="widgetType === 'github-profile-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GitHubTrending
v-else-if="widgetType === 'github-trending-repos'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlAlerts
v-else-if="widgetType === 'gl-alerts'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlCpuCores
v-else-if="widgetType === 'gl-current-cores'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlCpuGauge
v-else-if="widgetType === 'gl-current-cpu'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlCpuHistory
v-else-if="widgetType === 'gl-cpu-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlDiskIo
v-else-if="widgetType === 'gl-disk-io'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlDiskSpace
v-else-if="widgetType === 'gl-disk-space'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlIpAddress
v-else-if="widgetType === 'gl-ip-address'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlLoadHistory
v-else-if="widgetType === 'gl-load-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlMemGauge
v-else-if="widgetType === 'gl-current-mem'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlMemHistory
v-else-if="widgetType === 'gl-mem-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlNetworkInterfaces
v-else-if="widgetType === 'gl-network-interfaces'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlNetworkTraffic
v-else-if="widgetType === 'gl-network-traffic'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<GlSystemLoad
v-else-if="widgetType === 'gl-system-load'"
:options="widgetOptions"
@loading="setLoaderState"
@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"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<IframeWidget
v-else-if="widgetType === 'iframe'"
:options="widgetOptions"
@loading="setLoaderState"
@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"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<MullvadStatus
v-else-if="widgetType === 'mullvad-status'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<NdCpuHistory
v-else-if="widgetType === 'nd-cpu-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<NdLoadHistory
v-else-if="widgetType === 'nd-load-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<NdRamHistory
v-else-if="widgetType === 'nd-ram-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<NewsHeadlines
v-else-if="widgetType === 'news-headlines'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PiHoleStats
v-else-if="widgetType === 'pi-hole-stats'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PiHoleTopQueries
v-else-if="widgetType === 'pi-hole-top-queries'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PiHoleTraffic
v-else-if="widgetType === 'pi-hole-traffic'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PublicHolidays
v-else-if="widgetType === 'public-holidays'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<PublicIp
v-else-if="widgetType === 'public-ip'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<RssFeed
v-else-if="widgetType === 'rss-feed'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<SportsScores
v-else-if="widgetType === 'sports-scores'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<StatPing
v-else-if="widgetType === 'stat-ping'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<StockPriceChart
v-else-if="widgetType === 'stock-price-chart'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<SynologyDownload
v-else-if="widgetType === 'synology-download'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<SystemInfo
v-else-if="widgetType === 'system-info'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<TflStatus
v-else-if="widgetType === 'tfl-status'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<WalletBalance
v-else-if="widgetType === 'wallet-balance'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Weather
v-else-if="widgetType === 'weather'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<WeatherForecast
v-else-if="widgetType === 'weather-forecast'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<XkcdComic
v-else-if="widgetType === 'xkcd-comic'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<!-- No widget type specified -->
<div v-else>{{ handleError('Widget type was not found') }}</div>
</div>
</div>
</template>
@@ -447,6 +39,74 @@ import UpdateIcon from '@/assets/interface-icons/widget-update.svg';
import OpenIcon from '@/assets/interface-icons/open-new-tab.svg';
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
const COMPAT = {
'adguard-dns-info': 'AdGuardDnsInfo',
'adguard-filter-status': 'AdGuardFilterStatus',
'adguard-stats': 'AdGuardStats',
'adguard-top-domains': 'AdGuardTopDomains',
anonaddy: 'AnonAddy',
apod: 'Apod',
'blacklist-check': 'BlacklistCheck',
clock: 'Clock',
'crypto-price-chart': 'CryptoPriceChart',
'crypto-watch-list': 'CryptoWatchList',
'cve-vulnerabilities': 'CveVulnerabilities',
'domain-monitor': 'DomainMonitor',
'code-stats': 'CodeStats',
'covid-stats': 'CovidStats',
embed: 'EmbedWidget',
'eth-gas-prices': 'EthGasPrices',
'exchange-rates': 'ExchangeRates',
'flight-data': 'Flights',
'github-profile-stats': 'GitHubProfile',
'github-trending-repos': 'GitHubTrending',
'gl-alerts': 'GlAlerts',
'gl-current-cores': 'GlCpuCores',
'gl-current-cpu': 'GlCpuGauge',
'gl-cpu-history': 'GlCpuHistory',
'gl-disk-io': 'GlDiskIo',
'gl-disk-space': 'GlDiskSpace',
'gl-ip-address': 'GlIpAddress',
'gl-load-history': 'GlLoadHistory',
'gl-current-mem': 'GlMemGauge',
'gl-mem-history': 'GlMemHistory',
'gl-network-interfaces': 'GlNetworkInterfaces',
'gl-network-traffic': 'GlNetworkTraffic',
'gl-system-load': 'GlSystemLoad',
'gl-cpu-temp': 'GlCpuTemp',
'health-checks': 'HealthChecks',
iframe: 'IframeWidget',
image: 'ImageWidget',
joke: 'Jokes',
'mullvad-status': 'MullvadStatus',
'nd-cpu-history': 'NdCpuHistory',
'nd-load-history': 'NdLoadHistory',
'nd-ram-history': 'NdRamHistory',
'news-headlines': 'NewsHeadlines',
'nextcloud-notifications': 'NextcloudNotifications',
'nextcloud-php-opcache': 'NextcloudPhpOpcache',
'nextcloud-stats': 'NextcloudStats',
'nextcloud-system': 'NextcloudSystem',
'nextcloud-user': 'NextcloudUser',
'nextcloud-user-status': 'NextcloudUserStatus',
'pi-hole-stats': 'PiHoleStats',
'pi-hole-top-queries': 'PiHoleTopQueries',
'pi-hole-traffic': 'PiHoleTraffic',
'public-holidays': 'PublicHolidays',
'public-ip': 'PublicIp',
'rss-feed': 'RssFeed',
'sports-scores': 'SportsScores',
'stat-ping': 'StatPing',
'stock-price-chart': 'StockPriceChart',
'synology-download': 'SynologyDownload',
'system-info': 'SystemInfo',
'tfl-status': 'TflStatus',
'wallet-balance': 'WalletBalance',
weather: 'Weather',
'weather-forecast': 'WeatherForecast',
'xkcd-comic': 'XkcdComic',
};
export default {
name: 'Widget',
components: {
@@ -455,66 +115,6 @@ export default {
UpdateIcon,
OpenIcon,
LoadingAnimation,
// Register widget components
AdGuardDnsInfo: () => import('@/components/Widgets/AdGuardDnsInfo.vue'),
AdGuardFilterStatus: () => import('@/components/Widgets/AdGuardFilterStatus.vue'),
AdGuardStats: () => import('@/components/Widgets/AdGuardStats.vue'),
AdGuardTopDomains: () => import('@/components/Widgets/AdGuardTopDomains.vue'),
AnonAddy: () => import('@/components/Widgets/AnonAddy.vue'),
Apod: () => import('@/components/Widgets/Apod.vue'),
BlacklistCheck: () => import('@/components/Widgets/BlacklistCheck.vue'),
Clock: () => import('@/components/Widgets/Clock.vue'),
CodeStats: () => import('@/components/Widgets/CodeStats.vue'),
CovidStats: () => import('@/components/Widgets/CovidStats.vue'),
CryptoPriceChart: () => import('@/components/Widgets/CryptoPriceChart.vue'),
CryptoWatchList: () => import('@/components/Widgets/CryptoWatchList.vue'),
CveVulnerabilities: () => import('@/components/Widgets/CveVulnerabilities.vue'),
DomainMonitor: () => import('@/components/Widgets/DomainMonitor.vue'),
EmbedWidget: () => import('@/components/Widgets/EmbedWidget.vue'),
EthGasPrices: () => import('@/components/Widgets/EthGasPrices.vue'),
ExchangeRates: () => import('@/components/Widgets/ExchangeRates.vue'),
Flights: () => import('@/components/Widgets/Flights.vue'),
GitHubTrending: () => import('@/components/Widgets/GitHubTrending.vue'),
GitHubProfile: () => import('@/components/Widgets/GitHubProfile.vue'),
GlAlerts: () => import('@/components/Widgets/GlAlerts.vue'),
GlCpuCores: () => import('@/components/Widgets/GlCpuCores.vue'),
GlCpuGauge: () => import('@/components/Widgets/GlCpuGauge.vue'),
GlCpuHistory: () => import('@/components/Widgets/GlCpuHistory.vue'),
GlDiskIo: () => import('@/components/Widgets/GlDiskIo.vue'),
GlDiskSpace: () => import('@/components/Widgets/GlDiskSpace.vue'),
GlIpAddress: () => import('@/components/Widgets/GlIpAddress.vue'),
GlLoadHistory: () => import('@/components/Widgets/GlLoadHistory.vue'),
GlMemGauge: () => import('@/components/Widgets/GlMemGauge.vue'),
GlMemHistory: () => import('@/components/Widgets/GlMemHistory.vue'),
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'),
MullvadStatus: () => import('@/components/Widgets/MullvadStatus.vue'),
NdCpuHistory: () => import('@/components/Widgets/NdCpuHistory.vue'),
NdLoadHistory: () => import('@/components/Widgets/NdLoadHistory.vue'),
NdRamHistory: () => import('@/components/Widgets/NdRamHistory.vue'),
NewsHeadlines: () => import('@/components/Widgets/NewsHeadlines.vue'),
PiHoleStats: () => import('@/components/Widgets/PiHoleStats.vue'),
PiHoleTopQueries: () => import('@/components/Widgets/PiHoleTopQueries.vue'),
PiHoleTraffic: () => import('@/components/Widgets/PiHoleTraffic.vue'),
PublicHolidays: () => import('@/components/Widgets/PublicHolidays.vue'),
PublicIp: () => import('@/components/Widgets/PublicIp.vue'),
RssFeed: () => import('@/components/Widgets/RssFeed.vue'),
SportsScores: () => import('@/components/Widgets/SportsScores.vue'),
StatPing: () => import('@/components/Widgets/StatPing.vue'),
StockPriceChart: () => import('@/components/Widgets/StockPriceChart.vue'),
SynologyDownload: () => import('@/components/Widgets/SynologyDownload.vue'),
SystemInfo: () => import('@/components/Widgets/SystemInfo.vue'),
TflStatus: () => import('@/components/Widgets/TflStatus.vue'),
WalletBalance: () => import('@/components/Widgets/WalletBalance.vue'),
Weather: () => import('@/components/Widgets/Weather.vue'),
WeatherForecast: () => import('@/components/Widgets/WeatherForecast.vue'),
XkcdComic: () => import('@/components/Widgets/XkcdComic.vue'),
},
props: {
widget: Object,
@@ -556,6 +156,15 @@ export default {
hideControls() {
return this.widget.hideControls;
},
component() {
const type = COMPAT[this.widgetType] || this.widget.type;
if (!type) {
ErrorHandler('Widget type was not found');
return null;
}
// eslint-disable-next-line prefer-template
return () => import('@/components/Widgets/' + type + '.vue').catch(() => import('@/components/Widgets/Blank.vue'));
},
},
methods: {
/* Calls update data method on widget */