🚧 Refactor + more widgets
* ♻️ segment into smaller widgets, improve mixin * ♻️ change NextcloudInfo to NextcloudUser * a small widget showing branding and uesr info, including quota * ✨ add NextcloudNotifications widget * show and delete Nextcloud notifications * ✨ add NextcloudUserStatus widget * display user statuses of selected users * ✨ add NextcloudStats widget (admin only) * display Nextcloud usage statistics (users, files, shares) * ✨ add NextcloudSystem widget (admin only) * visualise cpu load and memory utilisation, show server versions * ✨ add NextcloudPhpOpcache widget (admin only) * show statistics about php opcache performance * ✨ add a misc helper for formatting nunbers * 🌐 add translations to widget templates * 🌐 add translation entries for en * 🍱 add scss styles file, shared by all widgets
This commit is contained in:
@@ -1,381 +0,0 @@
|
||||
<template>
|
||||
<div class="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>{{ $t('widgets.nextcloud-info.label-version') }} {{ version.string }}</small>
|
||||
</p>
|
||||
<p class="username">{{ user.displayName }} <em v-if="user.id">({{ user.id }})</em></p>
|
||||
<p class="login" v-tooltip="lastLoginTooltip()">
|
||||
<span>{{ $t('widgets.nextcloud-info.label-last-login') }}</span>
|
||||
<small>{{ getTimeAgo(user.lastLogin) }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- disk quota -->
|
||||
<div v-if="user.quota.quota > 0" v-tooltip="quotaTooltip()">
|
||||
<p>
|
||||
<i class="fal fa-disc-drive"></i>
|
||||
<strong>{{ $t('Disk Quota') }}</strong>
|
||||
<em>{{ user.quota.relative }}%</em>
|
||||
<small>of</small> <strong>{{ convertBytes(user.quota.total) }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="user.isAdmin" class="server-info">
|
||||
<!-- server info: users -->
|
||||
<div>
|
||||
<p v-tooltip="activeUsersTooltip()">
|
||||
<i class="fal fa-user"></i>
|
||||
<em>{{ formatNumber(server.nextcloud.storage.num_users) }}</em>
|
||||
<strong>{{ $t('total users') }}</strong> <small>{{ $t('of which') }}</small>
|
||||
<em>{{ formatNumber(server.activeUsers.last24hours) }}</em>
|
||||
<strong>{{ $t('active') }}</strong> <small>({{ $t('last 24 hours') }})</small>
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<!-- server info: apps -->
|
||||
<p>
|
||||
<i class="fal fa-browser"></i>
|
||||
<em>{{ formatNumber(server.nextcloud.system.apps.num_installed) }}</em>
|
||||
<strong>{{ $t('applications') }}</strong>
|
||||
<span v-if="server.nextcloud.system.apps.num_updates_available"
|
||||
data-has-updates v-tooltip="appUpdatesTooltip()">
|
||||
<i class="fal fa-download"></i>
|
||||
<em>{{ server.nextcloud.system.apps.num_updates_available }}</em>
|
||||
<strong>
|
||||
{{ $t('widgets.nextcloud-info.updates-available',
|
||||
{plural: server.nextcloud.system.apps.num_updates_available > 1 ? 's' : ''}) }}
|
||||
</strong>
|
||||
</span>
|
||||
<span v-else >
|
||||
{{ $t('no pending updates') }}
|
||||
</span>
|
||||
</p>
|
||||
<hr />
|
||||
<!-- server info: storage -->
|
||||
<p v-tooltip="storagesTooltip()">
|
||||
<i class="fal fa-file"></i>
|
||||
<em>{{ formatNumber(server.nextcloud.storage.num_files) }}</em>
|
||||
<strong>{{ $t('files') }}</strong> <small>{{ $t('in') }}</small>
|
||||
<em>{{ server.nextcloud.storage.num_storages }}</em>
|
||||
<strong>{{ $t('storages') }}</strong> |
|
||||
<strong>{{ convertBytes(server.nextcloud.system.freespace) }}</strong>
|
||||
<small>{{ $t('free') }}</small>
|
||||
</p>
|
||||
<hr />
|
||||
<!-- server info: shares -->
|
||||
<p v-tooltip="sharesTooltip()">
|
||||
<i class="fal fa-share"></i>
|
||||
<em>{{ formatNumber(server.nextcloud.shares.num_shares) }}</em>
|
||||
<strong>{{ $t('autonomous') }}</strong> <small> {{ $t('and') }}</small>
|
||||
<em>
|
||||
{{ formatNumber(server.nextcloud.shares.num_fed_shares_sent +
|
||||
server.nextcloud.shares.num_fed_shares_received) }}
|
||||
</em>
|
||||
<strong>{{ $t('federated shares') }}</strong>
|
||||
</p>
|
||||
<hr />
|
||||
<!-- server info: server -->
|
||||
<p>
|
||||
<i class="fal fa-server"></i>
|
||||
<strong>{{ $t('Nextcloud') }}</strong>
|
||||
<em>{{ server.nextcloud.system.version }}</em> |
|
||||
<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>{{ $t('using') }}</small>
|
||||
<em>{{ convertBytes(server.server.database.size) }}</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import NextcloudMixin from '@/mixins/NextcloudMixin';
|
||||
import { convertBytes } from '@/utils/MiscHelpers';
|
||||
// //import { NcdServer } from '@/utils/ncd';
|
||||
|
||||
const NextcloudSchema = {
|
||||
branding: {
|
||||
name: null,
|
||||
logo: null,
|
||||
url: null,
|
||||
slogan: null,
|
||||
},
|
||||
version: {
|
||||
string: null,
|
||||
edition: null,
|
||||
},
|
||||
server: {
|
||||
server: {
|
||||
webserver: null,
|
||||
php: {
|
||||
version: null,
|
||||
},
|
||||
opCache: {
|
||||
enabled: false,
|
||||
full: false,
|
||||
stats: {
|
||||
num_cached_scripts: null,
|
||||
num_cached_keys: null,
|
||||
max_cached_keys: null,
|
||||
hits: null,
|
||||
start_time: null,
|
||||
last_restart_time: 0,
|
||||
misses: null,
|
||||
opcache_hit_rate: null,
|
||||
},
|
||||
memory: {
|
||||
used_memory: null,
|
||||
free_memory: null,
|
||||
wasted_memory: null,
|
||||
current_wasted_percentage: null,
|
||||
},
|
||||
},
|
||||
database: {
|
||||
type: null,
|
||||
version: null,
|
||||
size: null,
|
||||
},
|
||||
},
|
||||
nextcloud: {
|
||||
system: {
|
||||
version: null,
|
||||
freespace: null,
|
||||
cpuload: [],
|
||||
mem_total: null,
|
||||
mem_free: 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin, NextcloudMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return NextcloudSchema;
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
await this.loadCapabilities();
|
||||
await this.loadUser();
|
||||
if (this.user.isAdmin) {
|
||||
this.processServerInfo(
|
||||
await this.makeRequest(this.endpoint('serverinfo'), this.headers),
|
||||
// //NcdServer,
|
||||
);
|
||||
}
|
||||
this.finishLoading();
|
||||
},
|
||||
processServerInfo(serverData) {
|
||||
const data = serverData?.ocs?.data;
|
||||
if (!data) {
|
||||
this.error('Invalid response');
|
||||
return;
|
||||
}
|
||||
this.server.nextcloud = data?.nextcloud;
|
||||
this.server.server.php.version = data?.server?.php?.version;
|
||||
this.server.server.opCache.enabled = data?.server?.php?.opcache?.opcache_enabled;
|
||||
this.server.server.opCache.full = data?.server?.php?.opcache?.cache_full;
|
||||
this.server.server.opCache.stats = data?.server?.php?.opcache?.opcache_statistics;
|
||||
this.server.server.database = data?.server?.database;
|
||||
this.server.server.webserver = data?.server?.webserver;
|
||||
this.server.activeUsers = data?.activeUsers;
|
||||
},
|
||||
quotaTooltip() {
|
||||
const content = `${convertBytes(this.user.quota.used)} used<br>`
|
||||
+ `${convertBytes(this.user.quota.free)} free<br>`
|
||||
+ `${convertBytes(this.user.quota.total)} total`;
|
||||
return {
|
||||
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-user-quota',
|
||||
};
|
||||
},
|
||||
lastLoginTooltip() {
|
||||
const content = new Date(this.user.lastLogin).toLocaleString();
|
||||
return {
|
||||
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
|
||||
};
|
||||
},
|
||||
activeUsersTooltip() {
|
||||
const content = `${this.server.activeUsers.last5minutes} in the last 5 minutes<br>`
|
||||
+ `${this.server.activeUsers.last1hour} in the last 1 hour<br>`;
|
||||
return {
|
||||
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
|
||||
};
|
||||
},
|
||||
appUpdatesTooltip() {
|
||||
const content = 'Updates are available for:<br><br>'
|
||||
+ ` ${Object.entries(this.server.nextcloud.system.apps.app_updates).join('<br>')}`;
|
||||
return {
|
||||
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
|
||||
};
|
||||
},
|
||||
storagesTooltip() {
|
||||
const content = 'Storages by type:<br><br>'
|
||||
+ `${this.server.nextcloud.storage.num_storages_local} local<br>`
|
||||
+ `${this.server.nextcloud.storage.num_storages_home} home<br>`
|
||||
+ `${this.server.nextcloud.storage.num_storages_other} other<br><br>`
|
||||
+ `${this.server.nextcloud.storage.num_files} files`;
|
||||
return {
|
||||
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
|
||||
};
|
||||
},
|
||||
sharesTooltip() {
|
||||
const content = 'Autonomous shares:<br><br>'
|
||||
+ `${this.server.nextcloud.shares.num_shares_user} user<br>`
|
||||
+ `${this.server.nextcloud.shares.num_shares_groups} groups<br>`
|
||||
+ `${this.server.nextcloud.shares.num_shares_mail} email<br>`
|
||||
+ `${this.server.nextcloud.shares.num_shares_room} chat room<br>`
|
||||
+ `${this.server.nextcloud.shares.num_shares_link} private link<br>`
|
||||
+ `${this.server.nextcloud.shares.num_shares_link_no_password} public link<br>`
|
||||
+ '<br>Federated shares:<br><br>'
|
||||
+ `${this.server.nextcloud.shares.num_fed_shares_sent} sent<br>`
|
||||
+ `${this.server.nextcloud.shares.num_fed_shares_received} received<br>`;
|
||||
return {
|
||||
content, html: true, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.overrideUpdateInterval = 120;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.nextcloud-info-wrapper {
|
||||
> div:first-child {
|
||||
display: flex;
|
||||
}
|
||||
> div:not(:first-child) {
|
||||
border-top: 1px dashed var(--widget-text-color);
|
||||
width: 96%;
|
||||
padding: .4rem 0;
|
||||
margin: auto;
|
||||
> div:not(:first-child) {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
p {
|
||||
color: var(--widget-text-color);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
> div:first-child {
|
||||
min-height: 8em;
|
||||
}
|
||||
p i {
|
||||
font-size: 110%;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
p em {
|
||||
font-size: 110%;
|
||||
margin: 0 4px;
|
||||
font-weight: 800;
|
||||
}
|
||||
strong {
|
||||
font-weight: 800;
|
||||
font-size: 105%;
|
||||
margin-left: .25rem;
|
||||
}
|
||||
small {
|
||||
opacity: .66;
|
||||
}
|
||||
hr {
|
||||
color: var(--widget-text-color);
|
||||
border: none;
|
||||
border-top: 1px solid;
|
||||
margin-top: 0.8rem;
|
||||
margin-bottom: 0.8rem;
|
||||
opacity: .25;
|
||||
}
|
||||
div.logo {
|
||||
width: 40%;
|
||||
text-align: center;
|
||||
img {
|
||||
width: 8rem;
|
||||
}
|
||||
p {
|
||||
font-size: 90%;
|
||||
opacity: .85;
|
||||
}
|
||||
}
|
||||
div.info {
|
||||
width: 56%;
|
||||
p {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
p.brand {
|
||||
margin: 0;
|
||||
font-size: 135%;
|
||||
font-weight: 800;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
p.version small {
|
||||
font-size: 75%;
|
||||
}
|
||||
p.username {
|
||||
font-size: 110%;
|
||||
em {
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
p.login {
|
||||
span {
|
||||
font-size: 90%;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
div.server-info {
|
||||
span[data-has-updates] {
|
||||
color: var(--success);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
192
src/components/Widgets/NextcloudNotifications.vue
Normal file
192
src/components/Widgets/NextcloudNotifications.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="nextcloud-widget nextcloud-status-wrapper">
|
||||
<div v-if="notifications.length" class="sep">
|
||||
<!-- group actions: delete all -->
|
||||
<p v-if="canDeleteNotification('delete-all')">
|
||||
<span class="action group-action" @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';
|
||||
// //import { NcdNotif } from '@/utils/ncd';
|
||||
|
||||
/**
|
||||
* 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: [],
|
||||
};
|
||||
},
|
||||
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)
|
||||
// //Promise.resolve(NcdNotif)
|
||||
.then(this.processNotifications)
|
||||
.finally(this.finishLoading);
|
||||
},
|
||||
processNotifications(response) {
|
||||
const notifications = this.validateResponse(response);
|
||||
this.notifications = [];
|
||||
notifications.forEach((notification) => {
|
||||
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')
|
||||
// //Promise.resolve()
|
||||
.then(() => {
|
||||
this.notifications = [];
|
||||
});
|
||||
},
|
||||
deleteNotification(id) {
|
||||
this.makeRequest(`${this.endpoint('notifications')}/${id}`, this.headers, 'DELETE')
|
||||
// //Promise.resolve()
|
||||
.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: .25rem;
|
||||
}
|
||||
small.date {
|
||||
background: var(--widget-text-color);
|
||||
color: var(--widget-accent-color);
|
||||
border-radius: 4px;
|
||||
padding: .15rem .3rem;
|
||||
margin: .25rem .25rem .25rem 0;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
opacity: .66;
|
||||
}
|
||||
span.group-action {
|
||||
float: right;
|
||||
}
|
||||
span.action, span a.action {
|
||||
cursor: pointer;
|
||||
margin: .1rem 0 .1rem .5rem;
|
||||
padding: .15rem;
|
||||
border-radius: 4px;
|
||||
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: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
top: 1rem;
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
}
|
||||
div hr {
|
||||
margin-top: 0.3rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
205
src/components/Widgets/NextcloudPhpOpcache.vue
Normal file
205
src/components/Widgets/NextcloudPhpOpcache.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<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>
|
||||
<em v-if="opcache.opcache_enabled" class="success">
|
||||
{{ tt('enabled') }}
|
||||
</em>
|
||||
<em v-else class="disabled">{{ tt('disabled') }}</em>
|
||||
<strong v-if="opcache.cache_full" class="danger">
|
||||
<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>
|
||||
<small>{{ tt('hits') }}</small>
|
||||
<em v-html="formatNumber(opcache_stats.misses)"></em>
|
||||
<small>{{ tt('misses') }}</small>
|
||||
<em v-html="formatPercent(opcache_stats.opcache_hit_rate, 3)"></em>
|
||||
<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>
|
||||
<small>of</small>
|
||||
<em v-html="convertBytes(opcache.memory_usage.total_memory)"></em>
|
||||
<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>
|
||||
<small>{{ tt('strings-use') }}</small>
|
||||
<em v-html="formatPercent(opcache.interned_strings_usage.used_memory_percentage)"></em>
|
||||
<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';
|
||||
|
||||
const NextcloudSystemSchema = {
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* NextcloudPhpOpcache widget - Shows statistics about PHP opcache performance
|
||||
* Used endpoints
|
||||
* - serverinfo: requires Nextcloud admin user
|
||||
*/
|
||||
export default {
|
||||
mixins: [WidgetMixin, NextcloudMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return NextcloudSystemSchema;
|
||||
},
|
||||
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';
|
||||
</style>
|
||||
203
src/components/Widgets/NextcloudStats.vue
Normal file
203
src/components/Widgets/NextcloudStats.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<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" data-nc-updates class="success">
|
||||
<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>
|
||||
<small>{{ tt('in') }}</small><em>{{ storage.num_storages }}</em>
|
||||
<strong>{{ tt('storages', { plural: storage.num_storages > 1 ? 's' : '' }) }}</strong>
|
||||
• <strong v-html="convertBytes(system.freespace)"></strong>
|
||||
<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';
|
||||
// //import { NcdServer } from '@/utils/ncd';
|
||||
|
||||
/**
|
||||
* NextcloudStats widget - Shows statistics about Nextcloud usage
|
||||
* Used endpoints
|
||||
* - serverinfo: requires Nextcloud admin user
|
||||
*/
|
||||
const NextcloudStatsSchema = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin, NextcloudMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return NextcloudStatsSchema;
|
||||
},
|
||||
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)
|
||||
// //Promise.resolve(NcdServer)
|
||||
.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 {
|
||||
[data-nc-updates] {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
234
src/components/Widgets/NextcloudSystem.vue
Normal file
234
src/components/Widgets/NextcloudSystem.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<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';
|
||||
// //import { NcdServer } from '@/utils/ncd';
|
||||
|
||||
const NextcloudSystemSchema = {
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 NextcloudSystemSchema;
|
||||
},
|
||||
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)
|
||||
// //Promise.resolve(NcdServer)
|
||||
.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.$nextTick();
|
||||
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 '#f6f000';
|
||||
if (memPercent < 80) return '#fca016';
|
||||
if (memPercent < 100) return '#f80363';
|
||||
return '#272f4d';
|
||||
},
|
||||
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: #999999;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: .69rem 0 1rem 0;
|
||||
}
|
||||
small:last-child {
|
||||
margin-top: 16px;
|
||||
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.3rem;
|
||||
margin: 0.5rem 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
193
src/components/Widgets/NextcloudUser.vue
Normal file
193
src/components/Widgets/NextcloudUser.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div v-if="user.id" 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 v-if="user.id">({{ user.id }})</em></p>
|
||||
<p class="login" v-tooltip="lastLoginTooltip()">
|
||||
<span>{{ tt('last-login') }}</span>
|
||||
<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="quotaChartData">
|
||||
<PercentageChart :values="quotaChartData" :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';
|
||||
import { convertBytes } from '@/utils/MiscHelpers';
|
||||
// //import { NcdUsr } from '@/utils/ncd';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
},
|
||||
quotaChartData: null,
|
||||
};
|
||||
},
|
||||
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)
|
||||
// //return Promise.resolve(NcdUsr)
|
||||
.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;
|
||||
const used = parseFloat(this.user.quota.used / this.user.quota.total);
|
||||
const free = parseFloat(this.user.quota.free / this.user.quota.total);
|
||||
this.quotaChartData = [
|
||||
{ label: this.tt('used'), size: used, color: this.getQuotaChartColor(used) },
|
||||
{ label: this.tt('available'), size: free, color: '#20e253' },
|
||||
];
|
||||
},
|
||||
getQuotaChartColor(percent) {
|
||||
if (percent < 0.75) return '#272f4d';
|
||||
if (percent < 0.85) return '#fce216';
|
||||
if (percent < 0.95) return '#ff9000';
|
||||
return '#f80363';
|
||||
},
|
||||
/* Tooltip generators */
|
||||
quotaTooltip() {
|
||||
const quotaEnabled = this.user.quota.quota > 0;
|
||||
const content = `${this.tt('quota-enabled', { not: quotaEnabled ? '' : 'not ' })}`
|
||||
+ `<br><br>${convertBytes(this.user.quota.used)} ${this.tt('used')}<br>`
|
||||
+ `${convertBytes(this.user.quota.free)} ${this.tt('free')}<br>`
|
||||
+ `${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;
|
||||
},
|
||||
};
|
||||
</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 0.75rem;
|
||||
width: 5em;
|
||||
position: relative;
|
||||
top: 0.2rem;
|
||||
float: right;
|
||||
}
|
||||
div.logo {
|
||||
width: 40%;
|
||||
text-align: center;
|
||||
img {
|
||||
width: 8rem;
|
||||
}
|
||||
p {
|
||||
font-size: 90%;
|
||||
opacity: .85;
|
||||
}
|
||||
}
|
||||
div.info {
|
||||
width: 56%;
|
||||
p {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
p:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
p.brand {
|
||||
margin: 0;
|
||||
font-size: 135%;
|
||||
font-weight: 800;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
p.version small {
|
||||
font-size: 75%;
|
||||
}
|
||||
p.username {
|
||||
font-size: 110%;
|
||||
em {
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
p.login {
|
||||
span {
|
||||
font-size: 90%;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
195
src/components/Widgets/NextcloudUserStatus.vue
Normal file
195
src/components/Widgets/NextcloudUserStatus.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<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>
|
||||
<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 ' + getStatusClass(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';
|
||||
// //import { NcdStatusAll, NcdStatusSingle } from '@/utils/ncd';
|
||||
|
||||
// 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;
|
||||
},
|
||||
},
|
||||
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)
|
||||
// //Promise.resolve(NcdStatusAll)
|
||||
.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)
|
||||
// //Promise.resolve(NcdStatusSingle)
|
||||
.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;
|
||||
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) {
|
||||
this.newStatuses[status.userId] = status;
|
||||
}
|
||||
},
|
||||
getStatusClass(status) {
|
||||
switch (status) {
|
||||
case 'online': return 'success';
|
||||
case 'away': return 'warning';
|
||||
case 'dnd': return 'danger';
|
||||
case 'offline': default: return 'disabled';
|
||||
}
|
||||
},
|
||||
/* 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: .25rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
color: #aaaaaa;
|
||||
top: 0;
|
||||
opacity: .5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
> div {
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
div.user hr {
|
||||
margin-top: 0.3rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -321,8 +321,43 @@
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NextcloudInfo
|
||||
v-else-if="widgetType === 'nextcloud-info'"
|
||||
<NextcloudNotifications
|
||||
v-else-if="widgetType === 'nextcloud-notifications'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NextcloudPhpOpcache
|
||||
v-else-if="widgetType === 'nextcloud-php-opcache'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NextcloudStats
|
||||
v-else-if="widgetType === 'nextcloud-stats'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NextcloudSystem
|
||||
v-else-if="widgetType === 'nextcloud-system'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NextcloudUser
|
||||
v-else-if="widgetType === 'nextcloud-user'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NextcloudUserStatus
|
||||
v-else-if="widgetType === 'nextcloud-user-status'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
@@ -506,7 +541,12 @@ export default {
|
||||
NdLoadHistory: () => import('@/components/Widgets/NdLoadHistory.vue'),
|
||||
NdRamHistory: () => import('@/components/Widgets/NdRamHistory.vue'),
|
||||
NewsHeadlines: () => import('@/components/Widgets/NewsHeadlines.vue'),
|
||||
NextcloudInfo: () => import('@/components/Widgets/NextcloudInfo.vue'),
|
||||
NextcloudNotifications: () => import('@/components/Widgets/NextcloudNotifications.vue'),
|
||||
NextcloudPhpOpcache: () => import('@/components/Widgets/NextcloudPhpOpcache.vue'),
|
||||
NextcloudStats: () => import('@/components/Widgets/NextcloudStats.vue'),
|
||||
NextcloudSystem: () => import('@/components/Widgets/NextcloudSystem.vue'),
|
||||
NextcloudUser: () => import('@/components/Widgets/NextcloudUser.vue'),
|
||||
NextcloudUserStatus: () => import('@/components/Widgets/NextcloudUserStatus.vue'),
|
||||
PiHoleStats: () => import('@/components/Widgets/PiHoleStats.vue'),
|
||||
PiHoleTopQueries: () => import('@/components/Widgets/PiHoleTopQueries.vue'),
|
||||
PiHoleTraffic: () => import('@/components/Widgets/PiHoleTraffic.vue'),
|
||||
|
||||
Reference in New Issue
Block a user