mirror of https://github.com/Lissy93/dashy.git
🎉 Add Nextcloud widget
Add a widget supporting the `capabilites`, `user` and `serverinfo` Nextcloud APIs. Basic branding, user and quota information is always displayed and when the provided credentials are for and admin user then server information is also displayed. APIs: * [capabilities](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html#capabilities-api) * [user](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html#user-metadata) * [serverinfo](https://github.com/nextcloud/serverinfo)
This commit is contained in:
parent
e24fa10f0f
commit
0bf6fee180
|
@ -303,6 +303,10 @@
|
|||
"remaining": "Remaining",
|
||||
"up": "Up",
|
||||
"down": "Down"
|
||||
},
|
||||
"nextcloud-info": {
|
||||
"label-version": "Nextcloud version",
|
||||
"label-last-login": "Last login"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,398 @@
|
|||
<template>
|
||||
<div class="nextcloud-info-wrapper">
|
||||
<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">
|
||||
{{ $t('widgets.nextcloud-info.label-version') }} {{ version.string }}
|
||||
</p>
|
||||
<p class="username">{{ user.displayName }} <em v-if="user.id">({{ user.id }})</em></p>
|
||||
<p class="login" v-tooltip="lastLoginTooltip()">
|
||||
{{ $t('widgets.nextcloud-info.label-last-login') }}
|
||||
<small>{{ getTimeAgo(user.lastLogin) }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
<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('updates available') }}</strong>
|
||||
</span>
|
||||
<span v-else >
|
||||
{{ $t('no pending updates') }}
|
||||
</span>
|
||||
</p>
|
||||
<hr />
|
||||
<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 />
|
||||
<p v-tooltip="sharesTooltip()">
|
||||
<i class="fal fa-share"></i>
|
||||
<em>{{ formatNumber(server.nextcloud.shares.num_shares) }}</em>
|
||||
<strong>{{ $t('shares') }}</strong> <small> {{ $t('and') }}</small>
|
||||
<em>
|
||||
{{ formatNumber(server.nextcloud.shares.num_fed_shares_sent) }}
|
||||
/ {{ formatNumber(server.nextcloud.shares.num_fed_shares_received) }}
|
||||
</em>
|
||||
<strong>{{ $t('federated shares') }}</strong>
|
||||
</p>
|
||||
<hr />
|
||||
<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 />
|
||||
<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 { NcdUsr, NcdServer } from '@/utils/ncd';
|
||||
|
||||
const NextcloudSchema = {
|
||||
branding: {
|
||||
name: null,
|
||||
logo: null,
|
||||
url: null,
|
||||
slogan: null,
|
||||
},
|
||||
version: {
|
||||
string: null,
|
||||
edition: null,
|
||||
},
|
||||
user: {
|
||||
id: null,
|
||||
isAdmin: false,
|
||||
displayName: null,
|
||||
email: null,
|
||||
quota: {
|
||||
relative: null,
|
||||
total: null,
|
||||
used: null,
|
||||
free: null,
|
||||
quota: 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() {
|
||||
const promise = this.fetchCapabilities()
|
||||
.then(() => this.makeRequest(this.endpoint('user'), this.headers))
|
||||
// //.then(() => NcdUsr)
|
||||
.then(this.processUser);
|
||||
|
||||
await promise;
|
||||
|
||||
if (this.user.isAdmin) {
|
||||
promise.then(() => this.makeRequest(this.endpoint('serverinfo'), this.headers))
|
||||
// //promise.then(() => NcdServer)
|
||||
.then(this.processServerInfo);
|
||||
}
|
||||
|
||||
promise.finally(() => this.finishLoading());
|
||||
},
|
||||
processUser(userData) {
|
||||
const user = userData?.ocs?.data;
|
||||
if (!user) {
|
||||
this.error('Invalid response');
|
||||
return;
|
||||
}
|
||||
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;
|
||||
this.user.isAdmin = user.groups && user.groups.includes('admin');
|
||||
},
|
||||
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: '
|
||||
+ ` ${Object.keys(this.server.nextcloud.system.apps.app_updates).join(', ')}`;
|
||||
return {
|
||||
content, 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><sup>*</sup>Federated shares: sent/received';
|
||||
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;
|
||||
}
|
||||
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 0 .23rem 0;
|
||||
font-size: 135%;
|
||||
font-weight: 800;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
p.version {
|
||||
font-size: 80%;
|
||||
opacity: .66;
|
||||
}
|
||||
p.username {
|
||||
font-size: 110%;
|
||||
em {
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
p.login {
|
||||
font-size: 90%;
|
||||
small {
|
||||
opacity: .75;
|
||||
margin-left: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
div.server-info {
|
||||
span[data-has-updates] {
|
||||
color: var(--success);
|
||||
padding-left: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -321,6 +321,13 @@
|
|||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NextcloudInfo
|
||||
v-else-if="widgetType === 'nextcloud-info'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<PiHoleStats
|
||||
v-else-if="widgetType === 'pi-hole-stats'"
|
||||
:options="widgetOptions"
|
||||
|
@ -499,6 +506,7 @@ 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'),
|
||||
PiHoleStats: () => import('@/components/Widgets/PiHoleStats.vue'),
|
||||
PiHoleTopQueries: () => import('@/components/Widgets/PiHoleTopQueries.vue'),
|
||||
PiHoleTraffic: () => import('@/components/Widgets/PiHoleTraffic.vue'),
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { serviceEndpoints } from '@/utils/defaults';
|
||||
import { convertBytes, formatNumber, getTimeAgo } from '@/utils/MiscHelpers';
|
||||
// //import { NcdCap } from '@/utils/ncd';
|
||||
|
||||
/** Reusable mixin for Nextcloud widgets */
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
capabilities: {
|
||||
notifications: null,
|
||||
activity: null,
|
||||
},
|
||||
capabilitiesLastUpdated: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('A hostname is required');
|
||||
return this.options.hostname;
|
||||
},
|
||||
username() {
|
||||
if (!this.options.username) this.error('A username is required');
|
||||
return this.options.username;
|
||||
},
|
||||
password() {
|
||||
if (!this.options.password) this.error('An app-password is required');
|
||||
return this.options.password;
|
||||
},
|
||||
headers() {
|
||||
return {
|
||||
'OCS-APIREQUEST': true,
|
||||
Accept: 'application/json',
|
||||
Authorization: `Basic ${window.btoa(`${this.username}:${this.password}`)}`,
|
||||
};
|
||||
},
|
||||
proxyReqEndpoint() {
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
return `${baseUrl}${serviceEndpoints.corsProxy}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
endpoint(id) {
|
||||
const endpoints = {
|
||||
capabilities: `${this.hostname}/ocs/v1.php/cloud/capabilities`,
|
||||
user: `${this.hostname}/ocs/v1.php/cloud/users/${this.username}`,
|
||||
serverinfo: `${this.hostname}/ocs/v2.php/apps/serverinfo/api/v1/info`,
|
||||
};
|
||||
return endpoints[id];
|
||||
},
|
||||
fetchCapabilities() {
|
||||
const promise = Promise.resolve();
|
||||
if ((new Date().getTime()) - this.capabilitiesLastUpdated > 3600000) {
|
||||
promise.then(() => this.makeRequest(this.endpoint('capabilities'), this.headers))
|
||||
// //promise.then(() => NcdCap)
|
||||
.then(this.processCapabilities);
|
||||
}
|
||||
return promise;
|
||||
},
|
||||
processCapabilities(data) {
|
||||
const ocdata = data?.ocs?.data;
|
||||
if (!ocdata) {
|
||||
this.error('Invalid response');
|
||||
return;
|
||||
}
|
||||
this.branding = ocdata?.capabilities?.theming;
|
||||
this.capabilities.notifications = ocdata?.capabilities?.notifications?.['ocs-endpoints'];
|
||||
this.capabilities.activity = ocdata?.capabilities?.activity?.apiv2;
|
||||
this.version.string = ocdata?.version?.string;
|
||||
this.version.edition = ocdata?.version?.edition;
|
||||
this.capabilitiesLastUpdated = new Date().getTime();
|
||||
},
|
||||
formatNumber(number) {
|
||||
return formatNumber(number);
|
||||
},
|
||||
convertBytes(bytes) {
|
||||
return convertBytes(bytes);
|
||||
},
|
||||
getTimeAgo(time) {
|
||||
return getTimeAgo(time);
|
||||
},
|
||||
},
|
||||
};
|
|
@ -105,6 +105,15 @@ export const convertBytes = (bytes, decimals = 2) => {
|
|||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / (k ** i)).toFixed(decimals))} ${sizes[i]}`;
|
||||
};
|
||||
/* Returns a numbers shortened version with suffixes for thousand, million, billion
|
||||
and trillion, e.g. 105_411 => 105.4K, 4_294_967_295 => 4.3B */
|
||||
export const formatNumber = (number) => {
|
||||
if (number > -1000 && number < 1000) return number;
|
||||
const k = 1000;
|
||||
const units = ['', 'K', 'M', 'B', 'T'];
|
||||
const i = Math.floor(Math.log(number) / Math.log(k));
|
||||
return `${(number / (k ** i)).toFixed(1)}${units[i]}`;
|
||||
};
|
||||
|
||||
/* Round price to appropriate number of decimals */
|
||||
export const roundPrice = (price) => {
|
||||
|
|
Loading…
Reference in New Issue