🚧 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,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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user