mirror of
https://github.com/Lissy93/dashy.git
synced 2024-11-23 21:23:28 +03:00
🔀 Merge pull request #740 from marekful/FEATURE/nextcloud-widgets
Feature/nextcloud widgets Credit to @marekful
This commit is contained in:
commit
dbb261c859
224
docs/widgets.md
224
docs/widgets.md
@ -48,6 +48,12 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
|
||||
- [AdGuard Home Filters](#adguard-home-filters)
|
||||
- [AdGuard Home DNS Info](#adguard-home-dns-info)
|
||||
- [AdGuard Home Top Domains](#adguard-home-top-domains)
|
||||
- [Nextcloud User](#nextcloud-user)
|
||||
- [Nextcloud User Statuses](#nextcloud-user-statuses)
|
||||
- [Nextcloud Notifications](#nextcloud-notifications)
|
||||
- [Nextcloud System](#nextcloud-system)
|
||||
- [Nextcloud Stats](#nextcloud-stats)
|
||||
- [Nextcloud PHP Opcache](#nextcloud-php-opcache-stats)
|
||||
- **[System Resource Monitoring](#system-resource-monitoring)**
|
||||
- [CPU Usage Current](#current-cpu-usage)
|
||||
- [CPU Usage Per Core](#cpu-usage-per-core)
|
||||
@ -1564,6 +1570,224 @@ Fetches data from your [AdGuard Home](https://adguard.com/en/adguard-home/overvi
|
||||
|
||||
---
|
||||
|
||||
### Nextcloud User
|
||||
|
||||
Nextcloud is a [self hosted](https://nextcloud.com/install/#instructions-server) productivity platform, it can also be used free of charge with [hundreds of existing hosting providers](https://nextcloud.com/sign-up/) that offer a free Nextcloud account.
|
||||
|
||||
Displays branding information of a Nextcloud server (logo, url, slogan) and some user details (name, login name, last login, disk space or quota). Use with regular or admin user.
|
||||
|
||||
Shows quota usage when quota is enabled for the user or disk usage when not enabled.
|
||||
|
||||
Known issues: the User API incorrectly reports available disk space as total for admin users when quota is not enabled (which usually is the case for admins).
|
||||
|
||||
<p align="center"><img width="450" src="https://i.ibb.co/F8Fdm3t/nextcloud-user.png" alt="nextcloud-user" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL of the Nextcloud server
|
||||
**`username`** | `string` | Required | Nextcloud username
|
||||
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
|
||||
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: nextcloud-user
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: https://nextcloud.example.com
|
||||
username: alice
|
||||
password: xxxxx-xxxxx-xxxxx-xxxxx
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟢 Required
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
|
||||
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
|
||||
|
||||
---
|
||||
|
||||
### Nextcloud User Statuses
|
||||
|
||||
Show user statuses for selected users.
|
||||
|
||||
<p align="center"><img width="450" src="https://i.ibb.co/Lk4DFT5/nextcloud-userstatus.png" alt="nextcloud-userstatus" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL of the Nextcloud server
|
||||
**`username`** | `string` | Required | Nextcloud username
|
||||
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
|
||||
**`users`** | `array` | Required | Nextcloud User IDs to show statuses for, list size between `1` and `100`
|
||||
**`showEmpty`** | `boolean` | _Optional_ | Show statuses without a message, defaults to `true`
|
||||
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: nextcloud-userstatus
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: https://nextcloud.example.com
|
||||
username: alice
|
||||
password: xxxxx-xxxxx-xxxxx-xxxxx
|
||||
users: ['bob', 'alice']
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟢 Required
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
|
||||
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
|
||||
|
||||
---
|
||||
|
||||
### Nextcloud Notifications
|
||||
|
||||
Displays your notifications and allows deleting them.
|
||||
|
||||
<p align="center"><img width="450" src="https://i.ibb.co/yQCS51k/nextcloud-notifications.png" alt="nextcloud-notifications" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL of the Nextcloud server
|
||||
**`username`** | `string` | Required | Nextcloud username
|
||||
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
|
||||
**`limit`** | `number\|string` | _Optional_ | Limit displayed notifications either by count, e.g. `5` to show the 5 most recent, or by age, e.g. `1d` to only show notifications not older than a day. Accepted suffixes for age limit are `m`, `h` and `d`.
|
||||
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: nextcloud-userstatus
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: https://nextcloud.example.com
|
||||
username: alice
|
||||
password: xxxxx-xxxxx-xxxxx-xxxxx
|
||||
limit: 6h
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟢 Required
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
|
||||
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
|
||||
|
||||
---
|
||||
|
||||
### Nextcloud System
|
||||
|
||||
Visualises overall memory utilisation and CPU load averages, shows server versions.
|
||||
|
||||
<p align="center"><img width="450" src="https://i.ibb.co/KW4t6nG/nextcloud-system.png" alt="nextcloud-system" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL of the Nextcloud server
|
||||
**`username`** | `string` | Required | Must be a Nextcloud admin user
|
||||
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: nextcloud-system
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: https://nextcloud.example.com
|
||||
username: alice
|
||||
password: xxxxx-xxxxx-xxxxx-xxxxx
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟢 Required
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
|
||||
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
|
||||
|
||||
---
|
||||
|
||||
### Nextcloud Stats
|
||||
|
||||
Shows key usage statistics about your Nextcloud server.
|
||||
|
||||
<p align="center"><img width="450" src="https://i.ibb.co/pPXPQFB/nextcloud-stats.png" alt="nextcloud-stats" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL of the Nextcloud server
|
||||
**`username`** | `string` | Required | Must be a Nextcloud admin user
|
||||
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: nextcloud-stats
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: https://nextcloud.example.com
|
||||
username: alice
|
||||
password: xxxxx-xxxxx-xxxxx-xxxxx
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟢 Required
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
|
||||
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
|
||||
|
||||
---
|
||||
|
||||
### Nextcloud PHP Opcache Stats
|
||||
|
||||
Shows statistics about PHP Opcache perforamnce on your Nextcloud server.
|
||||
|
||||
<p align="center"><img width="450" src="https://i.ibb.co/xf6M4J2/nextcloud-phpopcache.png" alt="nextcloud-phpopcache" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL of the Nextcloud server
|
||||
**`username`** | `string` | Required | Must be a Nextcloud admin user
|
||||
**`password`** | `string` | Required | Nextcloud app-password (create one in Settings -> Security)
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: nextcloud-stats
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: https://nextcloud.example.com
|
||||
username: alice
|
||||
password: xxxxx-xxxxx-xxxxx-xxxxx
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟢 Required
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [Nextcloud](https://nextcloud.com))
|
||||
- **Privacy**: _See [Nextcloud Privacy Policy](https://nextcloud.com/privacy)_
|
||||
|
||||
---
|
||||
|
||||
## System Resource Monitoring
|
||||
|
||||
The easiest method for displaying system info and resource usage in Dashy is with [Glances](https://nicolargo.github.io/glances/).
|
||||
|
@ -303,6 +303,77 @@
|
||||
"remaining": "Remaining",
|
||||
"up": "Up",
|
||||
"down": "Down"
|
||||
},
|
||||
"nextcloud": {
|
||||
"active": "active",
|
||||
"and": "and",
|
||||
"applications": "applications",
|
||||
"available": "available",
|
||||
"away": "Away",
|
||||
"cache-full": "CACHE FULL",
|
||||
"chat-room": "chat room",
|
||||
"delete-all": "Deleta all",
|
||||
"delete-notification": "Delete notification",
|
||||
"disabled": "disabled",
|
||||
"disk-quota": "Disk Quota",
|
||||
"disk-space": "Disk Space",
|
||||
"dnd": "Do Not Distrub",
|
||||
"email": "email",
|
||||
"enabled": "enabled",
|
||||
"federated-shares-ucfirst": "Federated shares",
|
||||
"federated-shares": "federated shares",
|
||||
"files": "file{plural}",
|
||||
"free": "free",
|
||||
"groups": "groups",
|
||||
"hit-rate": "hit rate",
|
||||
"hits": "hits",
|
||||
"home": "home",
|
||||
"in": "in",
|
||||
"keys": "keys",
|
||||
"last-24-hours": "last 24 hours",
|
||||
"last-5-minutes": "in the last 5 minutes",
|
||||
"last-hour": "in the last hour",
|
||||
"last-login": "Last login",
|
||||
"last-restart": "Last restart",
|
||||
"load-averages": "Load Averages over all CPU cores",
|
||||
"local-shares": "Local shares",
|
||||
"local": "local",
|
||||
"max-keys": "max keys",
|
||||
"memory-used": "memory used",
|
||||
"memory-utilisation": "memory utilisation",
|
||||
"memory": "memory",
|
||||
"misses": "misses",
|
||||
"no-notifications": "No notifications",
|
||||
"no-pending-updates": "no pending updates",
|
||||
"nothing-to-show": "Nothing to show here at this time",
|
||||
"of-which": "of which",
|
||||
"of": "of",
|
||||
"offline": "Offline",
|
||||
"online": "Online",
|
||||
"other": "other",
|
||||
"overall": "Ovarall",
|
||||
"private-link": "private link",
|
||||
"public-link": "public link",
|
||||
"quota-enabled": "Disk Quota is {not}enabled for this user",
|
||||
"received": "received",
|
||||
"scripts": "scripts",
|
||||
"sent": "sent",
|
||||
"started": "Started",
|
||||
"storages-by-type": "Storages by type",
|
||||
"storages": "storage{plural}",
|
||||
"strings-use": "strings use",
|
||||
"tasks": "Tasks",
|
||||
"total-files": "total files",
|
||||
"total-users": "total users",
|
||||
"total": "total",
|
||||
"until": "Until",
|
||||
"updates-available-for": "Updates are available for",
|
||||
"updates-available": "update{plural} available",
|
||||
"used": "used",
|
||||
"user": "user",
|
||||
"using": "using",
|
||||
"version": "version",
|
||||
"wasted": "wasted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
208
src/components/Widgets/NextcloudNotifications.vue
Normal file
208
src/components/Widgets/NextcloudNotifications.vue
Normal file
@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div class="nextcloud-widget nextcloud-status-wrapper">
|
||||
<div v-if="notifications.length">
|
||||
<!-- group actions: delete all -->
|
||||
<p v-if="canDeleteNotification('delete-all')" class="group-action">
|
||||
<span class="action secondary" @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';
|
||||
|
||||
/**
|
||||
* 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: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/* Parse the limit user option to either an integer or to an integer + 'm', 'h' or 'd' */
|
||||
limit() {
|
||||
const lim = this.options.limit;
|
||||
const defaultLimit = [0, false];
|
||||
if (typeof lim === 'string') {
|
||||
const k = { m: 60, h: 60 * 60, d: 60 * 60 * 24 };
|
||||
const m = lim.match(/(\d+)([hmd])/);
|
||||
if (m.length !== 3) return defaultLimit;
|
||||
return [false, m[1] * k[m[2]] * 1000];
|
||||
}
|
||||
if (typeof lim === 'number') {
|
||||
return [parseInt(this.options.limit, 10) || 0, false];
|
||||
}
|
||||
return defaultLimit;
|
||||
},
|
||||
},
|
||||
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)
|
||||
.then(this.processNotifications)
|
||||
.finally(this.finishLoading);
|
||||
},
|
||||
processNotifications(response) {
|
||||
const notifications = this.validateResponse(response);
|
||||
const [limitCount, limitTime] = this.limit;
|
||||
this.notifications = [];
|
||||
notifications.forEach((notification) => {
|
||||
if (limitCount && this.notifications.length === limitCount) return; // count limit
|
||||
const notiTime = Date.parse(notification.datetime);
|
||||
const nowTime = new Date().getTime();
|
||||
if (limitTime && notiTime && nowTime - notiTime > limitTime) return; // time limit
|
||||
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')
|
||||
.then(() => {
|
||||
this.notifications = [];
|
||||
});
|
||||
},
|
||||
deleteNotification(id) {
|
||||
this.makeRequest(`${this.endpoint('notifications')}/${id}`, this.headers, 'DELETE')
|
||||
.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: .25em;
|
||||
}
|
||||
small.date {
|
||||
background: var(--widget-text-color);
|
||||
color: var(--widget-accent-color);
|
||||
border-radius: .25em;
|
||||
padding: .15em .3em;
|
||||
margin: .25em .25em .25em 0;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
p.group-action {
|
||||
margin-top: 0;
|
||||
}
|
||||
span.action, span a.action {
|
||||
cursor: pointer;
|
||||
margin: .1em .5em .1em 0;
|
||||
padding: .15em;
|
||||
border-radius: .25em;
|
||||
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: 1em;
|
||||
position: relative;
|
||||
top: 1em;
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
}
|
||||
div hr {
|
||||
margin-top: .3em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
203
src/components/Widgets/NextcloudPhpOpcache.vue
Normal file
203
src/components/Widgets/NextcloudPhpOpcache.vue
Normal file
@ -0,0 +1,203 @@
|
||||
<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';
|
||||
|
||||
/**
|
||||
* NextcloudPhpOpcache widget - Shows statistics about PHP opcache performance
|
||||
* Used endpoints
|
||||
* - serverinfo: requires Nextcloud admin user
|
||||
*/
|
||||
export default {
|
||||
mixins: [WidgetMixin, NextcloudMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
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>
|
198
src/components/Widgets/NextcloudStats.vue
Normal file
198
src/components/Widgets/NextcloudStats.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<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" class="success has-updates">
|
||||
<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';
|
||||
|
||||
/**
|
||||
* NextcloudStats widget - Shows statistics about Nextcloud usage
|
||||
* Used endpoints
|
||||
* - serverinfo: requires Nextcloud admin user
|
||||
*/
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin, NextcloudMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
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,
|
||||
},
|
||||
};
|
||||
},
|
||||
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)
|
||||
.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 .nc-updates {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
</style>
|
230
src/components/Widgets/NextcloudSystem.vue
Normal file
230
src/components/Widgets/NextcloudSystem.vue
Normal file
@ -0,0 +1,230 @@
|
||||
<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';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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',
|
||||
},
|
||||
};
|
||||
},
|
||||
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)
|
||||
.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.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 this.getColorRgba('warning', 0.75);
|
||||
if (memPercent < 80) return this.getColorRgba('error', 0.9);
|
||||
if (memPercent < 100) return this.getColorRgba('danger');
|
||||
return this.getColorRgba('background');
|
||||
},
|
||||
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: #666666;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: .9em 0 1.4em 0;
|
||||
opacity: 1;
|
||||
}
|
||||
small:last-child {
|
||||
margin-top: 2em;
|
||||
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.3em;
|
||||
margin: .5em 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
206
src/components/Widgets/NextcloudUser.vue
Normal file
206
src/components/Widgets/NextcloudUser.vue
Normal file
@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div v-if="didLoadData" 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>({{ 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="quotaChart.dataLoaded">
|
||||
<PercentageChart :values="quotaChart.data" :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';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
},
|
||||
quotaChart: {
|
||||
dataLoaded: false,
|
||||
data: [
|
||||
{ label: null, size: null, color: null },
|
||||
{ label: null, size: null, color: null },
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
didLoadData() {
|
||||
return !!this.user.id;
|
||||
},
|
||||
},
|
||||
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)
|
||||
.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;
|
||||
},
|
||||
getQuotaChartColorUsed(percent) {
|
||||
if (percent < 0.75) return this.getValueFromCss('widget-text-color');
|
||||
if (percent < 0.85) return this.getValueFromCss('warning');
|
||||
if (percent < 0.95) return this.getValueFromCss('error');
|
||||
return this.getValueFromCss('danger');
|
||||
},
|
||||
updateQuotaChart() {
|
||||
const used = parseFloat(this.user.quota.used / this.user.quota.total);
|
||||
const free = parseFloat(this.user.quota.free / this.user.quota.total);
|
||||
const d = this.quotaChart.data;
|
||||
d[0] = { label: this.tt('used'), size: used, color: this.getQuotaChartColorUsed(used) };
|
||||
d[1] = { label: this.tt('available'), size: free, color: this.getValueFromCss('success') };
|
||||
this.quotaChart.dataLoaded = true;
|
||||
},
|
||||
/* Tooltip generators */
|
||||
quotaTooltip() {
|
||||
const quotaEnabled = this.user.quota.quota > 0;
|
||||
const content = `${this.tt('quota-enabled', { not: quotaEnabled ? '' : 'not ' })}`
|
||||
+ `<br><br>${this.convertBytes(this.user.quota.used)} ${this.tt('used')}<br>`
|
||||
+ `${this.convertBytes(this.user.quota.free)} ${this.tt('free')}<br>`
|
||||
+ `${this.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;
|
||||
},
|
||||
updated() {
|
||||
this.updateQuotaChart();
|
||||
},
|
||||
};
|
||||
</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 .75em;
|
||||
width: 5em;
|
||||
position: relative;
|
||||
top: .2em;
|
||||
float: right;
|
||||
}
|
||||
div.logo {
|
||||
width: 40%;
|
||||
text-align: center;
|
||||
img {
|
||||
width: 8em;
|
||||
}
|
||||
p {
|
||||
font-size: .9em;
|
||||
opacity: .85;
|
||||
}
|
||||
}
|
||||
div.info {
|
||||
width: 56%;
|
||||
p {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
p:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
p.brand {
|
||||
margin: 0;
|
||||
font-size: 1.35em;
|
||||
font-weight: 800;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
p.version small {
|
||||
font-size: .75em;
|
||||
}
|
||||
p.username {
|
||||
font-size: 1.1em;
|
||||
em {
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
||||
p.login {
|
||||
span {
|
||||
font-size: .9em;
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
202
src/components/Widgets/NextcloudUserStatus.vue
Normal file
202
src/components/Widgets/NextcloudUserStatus.vue
Normal file
@ -0,0 +1,202 @@
|
||||
<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 ${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';
|
||||
|
||||
// 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;
|
||||
},
|
||||
showEmpty() {
|
||||
return !!this.options.showEmpty;
|
||||
},
|
||||
},
|
||||
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)
|
||||
.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)
|
||||
.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;
|
||||
if (!status.message && !this.showEmpty) 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 && (status.message || this.showEmpty)) {
|
||||
this.newStatuses[status.userId] = status;
|
||||
}
|
||||
},
|
||||
/* 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: .15rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.online {
|
||||
color: var(--success);
|
||||
}
|
||||
.offline {
|
||||
color: var(--medium-grey);
|
||||
}
|
||||
.away {
|
||||
color: var(--error);
|
||||
}
|
||||
.dnd {
|
||||
color: var(--danger);
|
||||
}
|
||||
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 {
|
||||
top: 0;
|
||||
opacity: .5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
> div {
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
div.user hr {
|
||||
margin-top: .3em;
|
||||
margin-bottom: .3em;
|
||||
}
|
||||
div.user > div > div:last-child hr {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -321,6 +321,48 @@
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<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"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<PiHoleStats
|
||||
v-else-if="widgetType === 'pi-hole-stats'"
|
||||
:options="widgetOptions"
|
||||
@ -499,6 +541,12 @@ export default {
|
||||
NdLoadHistory: () => import('@/components/Widgets/NdLoadHistory.vue'),
|
||||
NdRamHistory: () => import('@/components/Widgets/NdRamHistory.vue'),
|
||||
NewsHeadlines: () => import('@/components/Widgets/NewsHeadlines.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'),
|
||||
|
208
src/mixins/NextcloudMixin.js
Normal file
208
src/mixins/NextcloudMixin.js
Normal file
@ -0,0 +1,208 @@
|
||||
import { serviceEndpoints } from '@/utils/defaults';
|
||||
import {
|
||||
convertBytes, formatNumber, getTimeAgo, timestampToDateTime,
|
||||
} from '@/utils/MiscHelpers';
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
enabled: null,
|
||||
features: [],
|
||||
},
|
||||
userStatus: null,
|
||||
},
|
||||
capabilitiesLastUpdated: 0,
|
||||
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 a Nextcloud app-password, not your login password.');
|
||||
return '';
|
||||
}
|
||||
return this.options.password;
|
||||
},
|
||||
/* HTTP headers for Nextcloud API requests */
|
||||
headers() {
|
||||
const authBase = `${this.username}:${this.password}`;
|
||||
return {
|
||||
'OCS-APIREQUEST': true,
|
||||
Accept: 'application/json',
|
||||
Authorization: `Basic ${window.btoa(authBase)}`,
|
||||
};
|
||||
},
|
||||
/* TTL for data delivered by the capabilities endpoint, ms */
|
||||
capabilitiesTtl() {
|
||||
return (parseInt(this.options.capabilitiesTtl, 10) || 3600) * 1000;
|
||||
},
|
||||
proxyReqEndpoint() {
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
return `${baseUrl}${serviceEndpoints.corsProxy}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Nextcloud API endpoints */
|
||||
endpoint(id) {
|
||||
switch (id) {
|
||||
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`;
|
||||
case 'capabilities':
|
||||
default:
|
||||
return `${this.hostname}/ocs/v1.php/cloud/capabilities`;
|
||||
}
|
||||
},
|
||||
/* 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)
|
||||
.then(this.processCapabilities);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
/* 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.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();
|
||||
},
|
||||
/* 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(/(-?\d+)((\.\d+)?\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(/(\d+)((\.\d+)?([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);
|
||||
},
|
||||
},
|
||||
};
|
64
src/styles/widgets/nextcloud-shared.scss
Normal file
64
src/styles/widgets/nextcloud-shared.scss
Normal file
@ -0,0 +1,64 @@
|
||||
.nextcloud-widget {
|
||||
p {
|
||||
color: var(--widget-text-color);
|
||||
margin: .5em 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
|
||||
p i {
|
||||
font-size: 1.1em;
|
||||
min-width: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p em {
|
||||
font-size: 1.1em;
|
||||
margin: 0 .24em;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 800;
|
||||
font-size: 1.05em;
|
||||
margin-left: .25em;
|
||||
}
|
||||
|
||||
small {
|
||||
opacity: .66;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: var(--widget-text-color);
|
||||
border: none;
|
||||
border-top: 1px solid;
|
||||
margin-top: .8em;
|
||||
margin-bottom: .8em;
|
||||
opacity: .25;
|
||||
clear: both;
|
||||
}
|
||||
hr:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.sep {
|
||||
border-top: 1px dashed var(--widget-text-color);
|
||||
width: 100%;
|
||||
padding: .4em 0 0 0;
|
||||
margin: .85em 0 0 0;
|
||||
> div:not(:first-child) {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep span.decimals {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
::v-deep div.percentage-chart {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
@ -106,6 +106,18 @@ export const convertBytes = (bytes, decimals = 2) => {
|
||||
return `${parseFloat((bytes / (k ** i)).toFixed(decimals))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
/* Round a number to thousands, millions, billions or trillions and suffix
|
||||
* with K, M, B or T respectively, e.g. 4_294_967_295 => 4.3B */
|
||||
export const formatNumber = (number, decimals = 1) => {
|
||||
if (number > -1000 && number < 1000) return number;
|
||||
const units = ['', 'K', 'M', 'B', 'T'];
|
||||
const k = 1000;
|
||||
const i = Math.floor(Math.log(number) / Math.log(k));
|
||||
const f = parseFloat(number / (k ** i));
|
||||
const d = f.toFixed(decimals) % 1.0 === 0 ? 0 : decimals; // number of decimals, omit .0
|
||||
return `${f.toFixed(d)}${units[i]}`;
|
||||
};
|
||||
|
||||
/* Round price to appropriate number of decimals */
|
||||
export const roundPrice = (price) => {
|
||||
if (Number.isNaN(price)) return price;
|
||||
|
Loading…
Reference in New Issue
Block a user