🚧 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:
Marcell Fülöp
2022-06-19 12:06:43 +00:00
parent a43988f3cd
commit 821af62426
12 changed files with 1558 additions and 442 deletions

View File

@@ -1,47 +1,64 @@
import { serviceEndpoints } from '@/utils/defaults';
import { convertBytes, formatNumber, getTimeAgo } from '@/utils/MiscHelpers';
// //import { NcdCap, NcdUsr } from '@/utils/ncd';
import {
convertBytes, formatNumber, getTimeAgo, timestampToDateTime,
} from '@/utils/MiscHelpers';
// //import { NcdCap } from '@/utils/ncd';
/** Reusable mixin for Nextcloud widgets */
/**
* Reusable mixin for Nextcloud widgets
* Nextcloud APIs
* - capabilities: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html#capabilities-api
* - userstatus: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-status-api.html#user-status-retrieve-statuses
* - user: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html#user-metadata
* - notifications: https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
* - serverinfo: https://github.com/nextcloud/serverinfo
*/
export default {
data() {
return {
validCredentials: null,
capabilities: {
notifications: null,
activity: null,
notifications: {
enabled: null,
features: [],
},
userStatus: null,
},
capabilitiesLastUpdated: 0,
user: {
id: null,
isAdmin: false,
displayName: null,
email: null,
quota: {
relative: null,
total: null,
used: null,
free: null,
quota: null,
},
branding: {
name: null,
logo: null,
url: null,
slogan: null,
},
version: {
string: null,
edition: null,
},
};
},
computed: {
/* The user provided Nextcloud hostname */
hostname() {
if (!this.options.hostname) this.error('A hostname is required');
return this.options.hostname;
},
/* The user provided Nextcloud username */
username() {
if (!this.options.username) this.error('A username is required');
return this.options.username;
},
/* The user provided Nextcloud password */
password() {
if (!this.options.password) this.error('An app-password is required');
// reject Nextcloud user passord (enforce 'app-password')
if (!/^([a-z0-9]{5}-){4}[a-z0-9]{5}$/i.test(this.options.password)) {
this.error('Please use an app-password for this widget, not your login password.');
this.error('Please use a Nextcloud app-password, not your login password.');
return '';
}
return this.options.password;
},
/* HTTP headers for Nextcloud API requests */
headers() {
return {
'OCS-APIREQUEST': true,
@@ -49,6 +66,7 @@ export default {
Authorization: `Basic ${window.btoa(`${this.username}:${this.password}`)}`,
};
},
/* TTL for data delivered by the capabilities endpoint, ms */
capabilitiesTtl() {
return (parseInt(this.options.capabilitiesTtl, 10) || 3600) * 1000;
},
@@ -58,6 +76,7 @@ export default {
},
},
methods: {
/* Nextcloud API endpoints */
endpoint(id) {
switch (id) {
case 'capabilities':
@@ -65,10 +84,65 @@ export default {
return `${this.hostname}/ocs/v1.php/cloud/capabilities`;
case 'user':
return `${this.hostname}/ocs/v1.php/cloud/users/${this.username}`;
case 'userstatus':
return `${this.hostname}/ocs/v2.php/apps/user_status/api/v1/statuses`;
case 'serverinfo':
return `${this.hostname}/ocs/v2.php/apps/serverinfo/api/v1/info`;
case 'notifications':
return `${this.hostname}/ocs/v2.php/apps/notifications/api/v2/notifications`;
}
},
/* Helper for widgets to terminate {fetchData} early */
hasValidCredentials() {
return this.validCredentials !== false
&& this.username.length > 0
&& this.password.length > 0;
},
/* Primary handler for every Nextcloud API response */
validateResponse(response) {
const data = response?.ocs?.data;
let meta = response?.ocs?.meta;
const error = response?.error; // Dashy error when cors-proxied
if (error && error.status) {
meta = { statuscode: error.status };
}
if (!meta || !meta.statuscode || !data) {
this.error('Invalid response');
}
switch (meta.statuscode) {
case 401:
this.validCredentials = false;
this.error(
`Access denied for user ${this.username}.`
+ ' Note that some Nextcloud widgets only work with an admin user.',
);
break;
case 429:
this.validCredentials = false;
this.error(
'The server indicated \'rate-limit reached\' error (HTTP 429).'
+ ' The server-info API may return this error for incorrect user/password.',
);
break;
case 993:
case 997:
case 998:
this.validCredentials = false;
this.error(
'The provided app-password is not permitted to access the requested resource or it has'
+ ' been revoked, or the username/password combination is incorrect',
);
break;
default:
this.validCredentials = true;
if (!this.allowedStatuscodes().includes(meta.statuscode)) {
this.error('Unexpected response');
}
break;
}
return data;
},
/* Process the capabilities endpoint if {capabilitiesTtl} has expired */
loadCapabilities() {
if ((new Date().getTime()) - this.capabilitiesLastUpdated > this.capabilitiesTtl) {
return this.makeRequest(this.endpoint('capabilities'), this.headers)
@@ -77,44 +151,59 @@ export default {
}
return Promise.resolve();
},
processCapabilities(data) {
const ocdata = data?.ocs?.data;
if (!ocdata) {
this.error('Invalid response');
return;
}
/* Update the sate based on the capabilites response */
processCapabilities(capResponse) {
const ocdata = this.validateResponse(capResponse);
const capNotif = ocdata?.capabilities?.notifications?.['ocs-endpoints'];
this.branding = ocdata?.capabilities?.theming;
this.capabilities.notifications = ocdata?.capabilities?.notifications?.['ocs-endpoints'];
this.capabilities.activity = ocdata?.capabilities?.activity?.apiv2;
this.capabilities.notifications.enabled = !!(capNotif?.length);
this.capabilities.notifications.features = capNotif || [];
this.capabilities.userStatus = !!(ocdata?.capabilities?.user_status?.enabled);
this.version.string = ocdata?.version?.string;
this.version.edition = ocdata?.version?.edition;
this.capabilitiesLastUpdated = new Date().getTime();
},
loadUser() {
return this.makeRequest(this.endpoint('user'), this.headers).then(this.processUser);
// //return Promise.resolve(NcdUsr).then(this.processUser);
},
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');
},
formatNumber(number) {
return formatNumber(number);
},
convertBytes(bytes) {
return convertBytes(bytes);
},
/* Shared template helpers */
getTimeAgo(time) {
return getTimeAgo(time);
},
formatDateTime(time) {
return timestampToDateTime(time);
},
/* Add additional formatting to {MiscHelpers.convertBytes()} */
convertBytes(bytes, decimals = 2, formatHtml = true) {
const formatted = convertBytes(bytes, decimals).toString();
if (!formatHtml) return formatted;
const m = formatted.match(/(-?[0-9]+)((\.[0-9]+)?\s(([KMGTPEZY]B|Bytes)))/);
return `${m[1]}<span class="decimals">${m[2]}</span>`;
},
/* Add additional formatting to {MiscHelpers.formatNumber()} */
formatNumber(number, decimals = 1, formatHtml = true) {
const formatted = formatNumber(number, decimals).toString();
if (!formatHtml) return formatted;
const m = formatted.match(/([0-9]+)((\.[0-9]+)?([KMBT]?))/);
return `${m[1]}<span class="decimals">${m[2]}</span>`;
},
/* Format a number as percentage value */
formatPercent(number, decimals = 2) {
const n = parseFloat(number).toFixed(decimals).split('.');
const d = n.length > 1 ? `.${n[1]}` : '';
return `${n[0]}<span class="decimals">${d}%</span>`;
},
/* Similar to {MiscHelpers.getValueFromCss()} but uses the widget root node to get
* the computed style so widget color is respected in variable widget color themes. */
getValueFromCss(colorVar) {
const cssProps = getComputedStyle(this.$el || document.documentElement);
return cssProps.getPropertyValue(`--${colorVar}`).trim();
},
/* Get {colorVar} CSS property value and return as rgba() */
getColorRgba(colorVar, alpha = 1) {
const [r, g, b] = this.getValueFromCss(colorVar).match(/\w\w/g).map(x => parseInt(x, 16));
return `rgba(${r},${g},${b},${alpha})`;
},
/* Translation shorthand with key prefix */
tt(key, options = null) {
return this.$t(`widgets.nextcloud.${key}`, options);
},
},
};