diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index a064eb67..2860c52c 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -303,6 +303,10 @@ "remaining": "Remaining", "up": "Up", "down": "Down" + }, + "nextcloud-info": { + "label-version": "Nextcloud version", + "label-last-login": "Last login" } } } diff --git a/src/components/Widgets/NextcloudInfo.vue b/src/components/Widgets/NextcloudInfo.vue new file mode 100644 index 00000000..ff19043b --- /dev/null +++ b/src/components/Widgets/NextcloudInfo.vue @@ -0,0 +1,398 @@ + + + + + diff --git a/src/components/Widgets/WidgetBase.vue b/src/components/Widgets/WidgetBase.vue index d34d135d..6551f265 100644 --- a/src/components/Widgets/WidgetBase.vue +++ b/src/components/Widgets/WidgetBase.vue @@ -321,6 +321,13 @@ @error="handleError" :ref="widgetRef" /> + 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'), diff --git a/src/mixins/NextcloudMixin.js b/src/mixins/NextcloudMixin.js new file mode 100644 index 00000000..6e040afb --- /dev/null +++ b/src/mixins/NextcloudMixin.js @@ -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); + }, + }, +}; diff --git a/src/utils/MiscHelpers.js b/src/utils/MiscHelpers.js index c3f59a8b..6a65222f 100644 --- a/src/utils/MiscHelpers.js +++ b/src/utils/MiscHelpers.js @@ -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) => {