mirror of https://github.com/Lissy93/dashy.git
🔀 Merge branch 'master' of github.com:Lissy93/dashy into REFACTOR/widget-and-docs-improvments
This commit is contained in:
commit
9235123f10
|
@ -13,6 +13,7 @@ Jeremy <hauvi> - 1 commits
|
|||
Kieren <onnel> - 1 commits
|
||||
Leonardo <olma> - 1 commits
|
||||
M <seno> - 1 commits
|
||||
Markus <raus> - 1 commits
|
||||
PlusaN <61884717+PlusaN@users.noreply.github.com> - 1 commits
|
||||
Rune <jørnerå> - 1 commits
|
||||
Ryan <urne> - 1 commits
|
||||
|
@ -24,7 +25,9 @@ deepsource-io[bot] <deepsource-io[bot]@users.noreply.github.com> - 1 commits
|
|||
dr460nf1r3 <njcrypted@protonmail.com> - 1 commits
|
||||
icy-comet <50461557+icy-comet@users.noreply.github.com> - 1 commits
|
||||
jnach <33467747+jnach@users.noreply.github.com> - 1 commits
|
||||
pablomalo <paul.gouin50@gmail.com> - 1 commits
|
||||
tazboyz16 <tazboyz_16@yahoo.com> - 1 commits
|
||||
zcq100 <m@zcq100.com> - 1 commits
|
||||
Alejandro <ina> - 2 commits
|
||||
Alessandro <e> - 2 commits
|
||||
BOZG <sr@bozg.se> - 2 commits
|
||||
|
@ -32,10 +35,13 @@ Brendan <'Lear> - 2 commits
|
|||
CHAIYEON <H> - 2 commits
|
||||
Dan <ilber> - 2 commits
|
||||
Ruben <ilv> - 2 commits
|
||||
k073l <21180271+k073l@users.noreply.github.com> - 2 commits
|
||||
liss-bot <87835202+liss-bot@users.noreply.github.com> - 2 commits
|
||||
patrickheeney <patrickheeney@gmail.com> - 2 commits
|
||||
ᗪєνιη <υн> - 2 commits
|
||||
Walkx <71191962+walkxcode@users.noreply.github.com> - 3 commits
|
||||
aterox <kenneth@kenneth.church> - 3 commits
|
||||
bogyeong <boggyhint@gmail.com> - 3 commits
|
||||
stanly0726 <37040069+stanly0726@users.noreply.github.com> - 3 commits
|
||||
Niklas <abe> - 4 commits
|
||||
Rémy <RANDI> - 4 commits
|
||||
|
@ -53,13 +59,14 @@ github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> - 16
|
|||
snyk-bot <snyk-bot@users.noreply.github.com> - 18 commits
|
||||
aterox <church.kennetha@gmail.com> - 19 commits
|
||||
EVOTk <45015615+EVOTk@users.noreply.github.com> - 22 commits
|
||||
Marcell <ülö> - 23 commits
|
||||
Alicia <yke> - 28 commits
|
||||
repo-visualizer <repo-visualizer@users.noreply.github.com> - 39 commits
|
||||
snyk-bot <snyk-bot@snyk.io> - 44 commits
|
||||
Alicia <o> - 78 commits
|
||||
repo-visualizer <repo-visualizer@users.noreply.github.com> - 43 commits
|
||||
snyk-bot <snyk-bot@snyk.io> - 50 commits
|
||||
Lissy93 <gh@d0h.co> - 78 commits
|
||||
liss-bot <liss-bot@d0h.co> - 89 commits
|
||||
Alicia <yke> - 123 commits
|
||||
Lissy93 <Lissy93@users.noreply.github.com> - 207 commits
|
||||
Alicia <yke> - 439 commits
|
||||
Alicia <o> - 84 commits
|
||||
liss-bot <liss-bot@d0h.co> - 95 commits
|
||||
Alicia <yke> - 148 commits
|
||||
Lissy93 <Lissy93@users.noreply.github.com> - 208 commits
|
||||
Alicia <yke> - 440 commits
|
||||
Alicia <yke> - 1488 commits
|
|
@ -1,37 +0,0 @@
|
|||
# Will add a comment and close any new issues opened by
|
||||
# users who have not yet committed to, or starred the repo
|
||||
name: 🎯 Issue Spam Control
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
jobs:
|
||||
check-user:
|
||||
if: >
|
||||
${{
|
||||
! contains( github.event.issue.labels.*.name, '📌 Keep Open') &&
|
||||
! contains( github.event.issue.labels.*.name, '🌈 Feedback') &&
|
||||
! contains( github.event.issue.labels.*.name, '💯 Showcase') &&
|
||||
github.event.comment.author_association != 'CONTRIBUTOR'
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
name: Close issue opened by non-stargazer
|
||||
steps:
|
||||
- name: close
|
||||
uses: uhyo/please-star-first@v1.0.1
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
message: |
|
||||
Welcome to Dashy 👋
|
||||
It's great to have you here, but unfortunately your ticket has been closed to prevent spam. Before reopening this issue, please ensure the following criteria are met.
|
||||
|
||||
Issues are sometimes closed when users:
|
||||
- Have only recently joined GitHub
|
||||
- Have not yet stared this repository
|
||||
- Have not previously interacted with the repo
|
||||
|
||||
Before you reopen this issue, please also ensure that:
|
||||
- You have checked that a similar issue does not already exist
|
||||
- You have checked the documentation for an existing solution
|
||||
- You have completed the relevant sections in the Issue template
|
||||
|
||||
Once you have verified the above standards are met, you may reopen this issue. Sorry for any inconvenience caused, I'm just a bot, and sometimes make mistakes 🤖
|
|
@ -0,0 +1,22 @@
|
|||
name: ⭐ Hello non-Stargazers
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
jobs:
|
||||
check-user:
|
||||
if: >
|
||||
${{
|
||||
! contains( github.event.issue.labels.*.name, '📌 Keep Open') &&
|
||||
! contains( github.event.issue.labels.*.name, '🌈 Feedback') &&
|
||||
! contains( github.event.issue.labels.*.name, '💯 Showcase') &&
|
||||
github.event.comment.author_association != 'CONTRIBUTOR'
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
name: Add comment to issues opened by non-stargazers
|
||||
steps:
|
||||
- name: comment
|
||||
uses: qxip/please-star-light@v4
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
autoclose: false
|
||||
message: "If you're enjoying Dashy, consider dropping us a ⭐<br>_<sub>🤖 I'm a bot, and this message was automated</sub>_"
|
5
Procfile
5
Procfile
|
@ -1,4 +1 @@
|
|||
# Heroku config - Specifies the commands to execute when the app starts
|
||||
# See docs for more info: https://devcenter.heroku.com/articles/procfile
|
||||
|
||||
web: node server.js
|
||||
web: npm run build-and-start
|
||||
|
|
36
README.md
36
README.md
|
@ -535,6 +535,13 @@ Huge thanks to the sponsors helping to support Dashy's development!
|
|||
<sub><b>Eddy Lazzarin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/UlisesGascon">
|
||||
<img src="https://avatars.githubusercontent.com/u/5110813?u=3c41facd8aa26154b9451de237c34b0f78d672a5&v=4" width="80;" alt="UlisesGascon"/>
|
||||
<br />
|
||||
<sub><b>Ulises Gascón</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/BOZG">
|
||||
<img src="https://avatars.githubusercontent.com/u/6022344?u=a52f42b946a1e1156f7bb9d7f65e9e28bb2da89f&v=4" width="80;" alt="BOZG"/>
|
||||
|
@ -542,13 +549,21 @@ Huge thanks to the sponsors helping to support Dashy's development!
|
|||
<sub><b>Stephen Rigney</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/bmcgonag">
|
||||
<img src="https://avatars.githubusercontent.com/u/7346620?u=2a0f9284f3e12ac1cc15288c254d1ec68a5081e8&v=4" width="80;" alt="bmcgonag"/>
|
||||
<br />
|
||||
<sub><b>Brian McGonagill</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Robert-Ernst">
|
||||
<img src="https://avatars.githubusercontent.com/u/9050259?u=7253b4063f1ffe3b5a894263c8b2056151802508&v=4" width="80;" alt="Robert-Ernst"/>
|
||||
<br />
|
||||
<sub><b>Robert Ernst</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/vlad-timofeev">
|
||||
<img src="https://avatars.githubusercontent.com/u/11474041?v=4" width="80;" alt="vlad-timofeev"/>
|
||||
|
@ -562,8 +577,14 @@ Huge thanks to the sponsors helping to support Dashy's development!
|
|||
<br />
|
||||
<sub><b>Kit L.</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mDafox">
|
||||
<img src="https://avatars.githubusercontent.com/u/21359974?v=4" width="80;" alt="mDafox"/>
|
||||
<br />
|
||||
<sub><b>Manu Devos</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Byolock">
|
||||
<img src="https://avatars.githubusercontent.com/u/25748003?v=4" width="80;" alt="Byolock"/>
|
||||
|
@ -584,7 +605,8 @@ Huge thanks to the sponsors helping to support Dashy's development!
|
|||
<br />
|
||||
<sub><b>Hugalafutro</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/shadowking001">
|
||||
<img src="https://avatars.githubusercontent.com/u/43928955?u=a00b44f22e5a82234d9b406ac048def1fbc16e31&v=4" width="80;" alt="shadowking001"/>
|
||||
|
@ -605,8 +627,7 @@ Huge thanks to the sponsors helping to support Dashy's development!
|
|||
<br />
|
||||
<sub><b>Robin Candau</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ced4568">
|
||||
<img src="https://avatars.githubusercontent.com/u/60725859?v=4" width="80;" alt="ced4568"/>
|
||||
|
@ -627,7 +648,8 @@ Huge thanks to the sponsors helping to support Dashy's development!
|
|||
<br />
|
||||
<sub><b>Undefined</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jtfinley72">
|
||||
<img src="https://avatars.githubusercontent.com/u/96497997?v=4" width="80;" alt="jtfinley72"/>
|
||||
|
|
3
app.json
3
app.json
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "Dashy",
|
||||
"website": "https://dashy.to/",
|
||||
"description": "A Dashboard for your Homelab 🚀",
|
||||
"repository": "https://github.com/lissy93/dashy",
|
||||
"logo": "https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/logo.png",
|
||||
|
@ -13,4 +14,4 @@
|
|||
"lissy93"
|
||||
],
|
||||
"stack": "heroku-20"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 8.7 MiB After Width: | Height: | Size: 9.9 MiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 106 KiB |
|
@ -18,6 +18,13 @@
|
|||
<sub><b>Eddy Lazzarin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/UlisesGascon">
|
||||
<img src="https://avatars.githubusercontent.com/u/5110813?u=3c41facd8aa26154b9451de237c34b0f78d672a5&v=4" width="80;" alt="UlisesGascon"/>
|
||||
<br />
|
||||
<sub><b>Ulises Gascón</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/BOZG">
|
||||
<img src="https://avatars.githubusercontent.com/u/6022344?u=a52f42b946a1e1156f7bb9d7f65e9e28bb2da89f&v=4" width="80;" alt="BOZG"/>
|
||||
|
@ -25,13 +32,21 @@
|
|||
<sub><b>Stephen Rigney</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/bmcgonag">
|
||||
<img src="https://avatars.githubusercontent.com/u/7346620?u=2a0f9284f3e12ac1cc15288c254d1ec68a5081e8&v=4" width="80;" alt="bmcgonag"/>
|
||||
<br />
|
||||
<sub><b>Brian McGonagill</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Robert-Ernst">
|
||||
<img src="https://avatars.githubusercontent.com/u/9050259?u=7253b4063f1ffe3b5a894263c8b2056151802508&v=4" width="80;" alt="Robert-Ernst"/>
|
||||
<br />
|
||||
<sub><b>Robert Ernst</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/vlad-timofeev">
|
||||
<img src="https://avatars.githubusercontent.com/u/11474041?v=4" width="80;" alt="vlad-timofeev"/>
|
||||
|
@ -45,8 +60,14 @@
|
|||
<br />
|
||||
<sub><b>Kit L.</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mDafox">
|
||||
<img src="https://avatars.githubusercontent.com/u/21359974?v=4" width="80;" alt="mDafox"/>
|
||||
<br />
|
||||
<sub><b>Manu Devos</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Byolock">
|
||||
<img src="https://avatars.githubusercontent.com/u/25748003?v=4" width="80;" alt="Byolock"/>
|
||||
|
@ -67,7 +88,8 @@
|
|||
<br />
|
||||
<sub><b>Hugalafutro</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/shadowking001">
|
||||
<img src="https://avatars.githubusercontent.com/u/43928955?u=a00b44f22e5a82234d9b406ac048def1fbc16e31&v=4" width="80;" alt="shadowking001"/>
|
||||
|
@ -88,8 +110,7 @@
|
|||
<br />
|
||||
<sub><b>Robin Candau</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ced4568">
|
||||
<img src="https://avatars.githubusercontent.com/u/60725859?v=4" width="80;" alt="ced4568"/>
|
||||
|
@ -110,7 +131,8 @@
|
|||
<br />
|
||||
<sub><b>Undefined</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jtfinley72">
|
||||
<img src="https://avatars.githubusercontent.com/u/96497997?v=4" width="80;" alt="jtfinley72"/>
|
||||
|
@ -203,14 +225,21 @@
|
|||
<sub><b>Remygrandin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/boggy-cs">
|
||||
<img src="https://avatars.githubusercontent.com/u/82003678?v=4" width="80;" alt="boggy-cs"/>
|
||||
<br />
|
||||
<sub><b>Bogyeong Kim</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/stanly0726">
|
||||
<img src="https://avatars.githubusercontent.com/u/37040069?v=4" width="80;" alt="stanly0726"/>
|
||||
<br />
|
||||
<sub><b>Stanly0726</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/onedr0p">
|
||||
<img src="https://avatars.githubusercontent.com/u/213795?v=4" width="80;" alt="onedr0p"/>
|
||||
|
@ -245,15 +274,15 @@
|
|||
<br />
|
||||
<sub><b>Dan Gilbert</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rubenandre">
|
||||
<img src="https://avatars.githubusercontent.com/u/9402773?v=4" width="80;" alt="rubenandre"/>
|
||||
<br />
|
||||
<sub><b>Rúben Silva</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Singebob">
|
||||
<img src="https://avatars.githubusercontent.com/u/24290044?v=4" width="80;" alt="Singebob"/>
|
||||
|
@ -288,15 +317,15 @@
|
|||
<br />
|
||||
<sub><b>DeepSource Bot</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/emiran-orange">
|
||||
<img src="https://avatars.githubusercontent.com/u/71817149?v=4" width="80;" alt="emiran-orange"/>
|
||||
<br />
|
||||
<sub><b>Emiran-orange</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/FormatToday">
|
||||
<img src="https://avatars.githubusercontent.com/u/20515769?v=4" width="80;" alt="FormatToday"/>
|
||||
|
@ -324,13 +353,6 @@
|
|||
<br />
|
||||
<sub><b>Jemy SCHNEPP</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/KierenConnell">
|
||||
<img src="https://avatars.githubusercontent.com/u/46445781?v=4" width="80;" alt="KierenConnell"/>
|
||||
<br />
|
||||
<sub><b>Kieren Connell</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<!-- readme: contributors -end -->
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||

|
||||
|
||||
### Running Dashy
|
||||
- [Quick Start](/docs/quick-start.md) - TDLR guide on getting Dashy up and running
|
||||
- [Quick Start](/docs/quick-start.md) - TLDR guide on getting Dashy up and running
|
||||
- [Deployment](/docs/deployment.md) - Full guide on deploying Dashy either locally or online
|
||||
- [Configuring](/docs/configuring.md) - Complete list of all available options in the config file
|
||||
- [App Management](/docs/management.md) - Managing your app, updating, security, web server configuration, etc
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# Troubleshooting
|
||||
|
||||
> _**This document contains common problems and their solutions.**_
|
||||
> _**This document contains common problems and their solutions.**_<br>
|
||||
> Please ensure your issue isn't listed here, before opening a new ticket.
|
||||
>
|
||||
> _If you came across an issue where the solution was not immediately obvious, consider adding it to this list to help other users._
|
||||
> _If you come across an issue not listed below, consider adding it, to help other users._
|
||||
|
||||
### Contents
|
||||
- [Refused to Connect in Web Content View](#refused-to-connect-in-modal-or-workspace-view)
|
||||
|
@ -32,6 +33,7 @@
|
|||
- [Weather Forecast Widget 401](#weather-forecast-widget-401)
|
||||
- [Font Awesome Icons not Displaying](#font-awesome-icons-not-displaying)
|
||||
- [Copy to Clipboard not Working](#copy-to-clipboard-not-working)
|
||||
- [How to Reset Local Settings](#how-to-reset-local-settings)
|
||||
- [How-To Open Browser Console](#how-to-open-browser-console)
|
||||
- [Git Contributions not Displaying](#git-contributions-not-displaying)
|
||||
|
||||
|
@ -94,10 +96,18 @@ If this works, but you wish to continue using HTML5 history mode, then a bit of
|
|||
|
||||
## 404 after Launch from Mobile Home Screen
|
||||
|
||||
Similar to the above issue, if you get a 404 after using iOS's “add to Home Screen” feature, then this is caused by Vue router.
|
||||
Similar to the above issue, if you get a 404 after using iOS and Android's “Add to Home Screen” feature, then this is caused by Vue router.
|
||||
It can be fixed by setting `appConfig.routingMode` to `hash`
|
||||
|
||||
See also: [#628](https://github.com/Lissy93/dashy/issues/628)
|
||||
See also: [#628](https://github.com/Lissy93/dashy/issues/628), [#762](https://github.com/Lissy93/dashy/issues/762)
|
||||
|
||||
---
|
||||
|
||||
## 404 On Multi-Page Apps
|
||||
|
||||
Similar to above, if you get a 404 error when visiting a page directly on multi-page apps, then this can be fixed under `appConfig`, by setting `routingMode` to `hash`. Then rebuilding, and refreshing the page.
|
||||
|
||||
See also: [#670](https://github.com/Lissy93/dashy/issues/670), [#763](https://github.com/Lissy93/dashy/issues/763)
|
||||
|
||||
---
|
||||
|
||||
|
@ -448,6 +458,21 @@ As a workaround, you could either:
|
|||
|
||||
---
|
||||
|
||||
## How to Reset Local Settings
|
||||
|
||||
Some settings are stored locally, in the browser's storage.
|
||||
|
||||
In some instances cached assets can prevent your settings from being updated, in which case you may wish to reset local data.
|
||||
|
||||
To clear all local data from the UI, head to the Config Menu, then click "Reset Local Settings", and Confirm when prompted.
|
||||
This will not affect your config file. But be sure that you keep a backup of your config, if you've not written changes it to disk.
|
||||
|
||||
You can also view any and all data that Dashy is storing, using the developer tools. Open your browser's dev tools (usually <kbd>F12</kbd>), in Chromium head to the Application tab, or in Firefox go to the Storage tab. Select Local Storage, then scroll down the the URL Dashy is running on. You should now see all data being stored, and you can select and delete any fields you wish.
|
||||
|
||||
For a full list of all data that may be cached, see the [Privacy Docs](/docs/privacy.md#browser-storage).
|
||||
|
||||
---
|
||||
|
||||
## How-To Open Browser Console
|
||||
When raising a bug, one crucial piece of info needed is the browser's console output. This will help the developer diagnose and fix the issue.
|
||||
|
||||
|
|
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)
|
||||
|
@ -1565,6 +1571,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/).
|
||||
|
|
|
@ -24,21 +24,23 @@ const printSuccess = () => {
|
|||
|
||||
// Check if the SSL certs are present and SSL should be enabled
|
||||
let enableSSL = false;
|
||||
stat(httpsCerts.public).then(() => {
|
||||
stat(httpsCerts.private).then(() => {
|
||||
const checkCertificateFiles = stat(httpsCerts.public).then(() => {
|
||||
return stat(httpsCerts.private).then(() => {
|
||||
enableSSL = true;
|
||||
}).catch(() => { printNotSoGood('Private key not present'); });
|
||||
}).catch(() => { printNotSoGood('Public key not present'); });
|
||||
|
||||
const startSSLServer = (app) => {
|
||||
// If SSL should be enabled, create a secured server and start it
|
||||
if (enableSSL) {
|
||||
const httpsServer = https.createServer({
|
||||
key: fs.readFileSync(httpsCerts.private),
|
||||
cert: fs.readFileSync(httpsCerts.public),
|
||||
}, app);
|
||||
httpsServer.listen(SSLPort, () => { printSuccess(); });
|
||||
}
|
||||
checkCertificateFiles.then(() => {
|
||||
// If SSL should be enabled, create a secured server and start it
|
||||
if (enableSSL) {
|
||||
const httpsServer = https.createServer({
|
||||
key: fs.readFileSync(httpsCerts.private),
|
||||
cert: fs.readFileSync(httpsCerts.public),
|
||||
}, app);
|
||||
httpsServer.listen(SSLPort, () => { printSuccess(); });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const middleware = (req, res, next) => {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,9 +175,13 @@ export default {
|
|||
if (this.enableStatusCheck) this.checkWebsiteStatus();
|
||||
// If continious status checking is enabled, then start ever-lasting loop
|
||||
if (this.statusCheckInterval > 0) {
|
||||
setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
|
||||
this.intervalId = setInterval(this.checkWebsiteStatus, this.statusCheckInterval * 1000);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// Stop periodic status-check when item is destroyed (e.g. navigating in multi-page setup)
|
||||
if (this.intervalId) clearInterval(this.intervalId);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<div class="apod-wrapper" v-if="image">
|
||||
<div class="apod-wrapper" v-if="url">
|
||||
<a :href="link" class="title" target="__blank" title="View Article">
|
||||
{{ title }}
|
||||
</a>
|
||||
<a :href="hdImage" title="View HD Image" class="picture" target="__blank">
|
||||
<img :src="image" :alt="title" />
|
||||
<a :href="hdurl" title="View HD Image" class="picture" target="__blank">
|
||||
<img :src="url" :alt="title" />
|
||||
</a>
|
||||
<p class="copyright">{{ copyright }}</p>
|
||||
<p class="description">{{ truncatedDescription }}</p>
|
||||
<p class="explanation">{{ truncatedExplanation }}</p>
|
||||
<p @click="toggleShowFull" class="expend-details-btn">
|
||||
{{ showFullDesc ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
|
||||
{{ showFullExp ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -24,17 +24,17 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
title: null,
|
||||
image: null,
|
||||
hdImage: null,
|
||||
link: null,
|
||||
description: null,
|
||||
url: null,
|
||||
hdurl: null,
|
||||
link: 'https://apod.nasa.gov/apod/astropix.html',
|
||||
explanation: null,
|
||||
copyright: null,
|
||||
showFullDesc: false,
|
||||
showFullExp: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
truncatedDescription() {
|
||||
return this.showFullDesc ? this.description : `${this.description.substring(0, 100)}...`;
|
||||
truncatedExplanation() {
|
||||
return this.showFullExp ? this.explanation : `${this.explanation.substring(0, 100)}...`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -52,14 +52,14 @@ export default {
|
|||
},
|
||||
processData(data) {
|
||||
this.title = data.title;
|
||||
this.image = data.url;
|
||||
this.hdImage = data.hdurl;
|
||||
this.link = data.apod_site;
|
||||
this.description = data.description;
|
||||
this.url = data.url;
|
||||
this.hdurl = data.hdurl;
|
||||
this.link = data.link;
|
||||
this.explanation = data.explanation;
|
||||
this.copyright = data.copyright;
|
||||
},
|
||||
toggleShowFull() {
|
||||
this.showFullDesc = !this.showFullDesc;
|
||||
this.showFullExp = !this.showFullExp;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -85,7 +85,7 @@ export default {
|
|||
opacity: var(--dimming-factor);
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
p.description {
|
||||
p.explanation {
|
||||
color: var(--widget-text-color);
|
||||
font-size: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,214 @@
|
|||
<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="oc-enabled">
|
||||
{{ tt('enabled') }}
|
||||
</em>
|
||||
<em v-else class="oc-disabled">{{ tt('disabled') }}</em>
|
||||
<strong v-if="opcache.cache_full" class="oc-full">
|
||||
<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';
|
||||
.nextcloud-phpopcache-wrapper {
|
||||
.oc-enabled {
|
||||
color: var(--success);
|
||||
}
|
||||
.oc-disabled {
|
||||
color: var(--neutral);
|
||||
}
|
||||
.oc-full {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,199 @@
|
|||
<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="nc-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 {
|
||||
color: var(--success);
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -20,421 +20,13 @@
|
|||
</div>
|
||||
<!-- Widget -->
|
||||
<div :class="`widget-wrap ${ error ? 'has-error' : '' }`">
|
||||
<AdGuardDnsInfo
|
||||
v-if="widgetType === 'adguard-dns-info'"
|
||||
<component
|
||||
v-bind:is="component"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<AdGuardFilterStatus
|
||||
v-else-if="widgetType === 'adguard-filter-status'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<AdGuardStats
|
||||
v-else-if="widgetType === 'adguard-stats'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<AdGuardTopDomains
|
||||
v-else-if="widgetType === 'adguard-top-domains'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<AnonAddy
|
||||
v-else-if="widgetType === 'anonaddy'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<Apod
|
||||
v-else-if="widgetType === 'apod'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<BlacklistCheck
|
||||
v-else-if="widgetType === 'blacklist-check'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<Clock
|
||||
v-else-if="widgetType === 'clock'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<CryptoPriceChart
|
||||
v-else-if="widgetType === 'crypto-price-chart'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<CryptoWatchList
|
||||
v-else-if="widgetType === 'crypto-watch-list'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<CveVulnerabilities
|
||||
v-else-if="widgetType === 'cve-vulnerabilities'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<DomainMonitor
|
||||
v-else-if="widgetType === 'domain-monitor'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<CodeStats
|
||||
v-else-if="widgetType === 'code-stats'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<CovidStats
|
||||
v-else-if="widgetType === 'covid-stats'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<EmbedWidget
|
||||
v-else-if="widgetType === 'embed'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<EthGasPrices
|
||||
v-else-if="widgetType === 'eth-gas-prices'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<ExchangeRates
|
||||
v-else-if="widgetType === 'exchange-rates'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<Flights
|
||||
v-else-if="widgetType === 'flight-data'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GitHubProfile
|
||||
v-else-if="widgetType === 'github-profile-stats'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GitHubTrending
|
||||
v-else-if="widgetType === 'github-trending-repos'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlAlerts
|
||||
v-else-if="widgetType === 'gl-alerts'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlCpuCores
|
||||
v-else-if="widgetType === 'gl-current-cores'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlCpuGauge
|
||||
v-else-if="widgetType === 'gl-current-cpu'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlCpuHistory
|
||||
v-else-if="widgetType === 'gl-cpu-history'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlDiskIo
|
||||
v-else-if="widgetType === 'gl-disk-io'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlDiskSpace
|
||||
v-else-if="widgetType === 'gl-disk-space'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlIpAddress
|
||||
v-else-if="widgetType === 'gl-ip-address'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlLoadHistory
|
||||
v-else-if="widgetType === 'gl-load-history'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlMemGauge
|
||||
v-else-if="widgetType === 'gl-current-mem'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlMemHistory
|
||||
v-else-if="widgetType === 'gl-mem-history'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlNetworkInterfaces
|
||||
v-else-if="widgetType === 'gl-network-interfaces'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlNetworkTraffic
|
||||
v-else-if="widgetType === 'gl-network-traffic'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlSystemLoad
|
||||
v-else-if="widgetType === 'gl-system-load'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<GlCpuTemp
|
||||
v-else-if="widgetType === 'gl-cpu-temp'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<HealthChecks
|
||||
v-else-if="widgetType === 'health-checks'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<IframeWidget
|
||||
v-else-if="widgetType === 'iframe'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<ImageWidget
|
||||
v-else-if="widgetType === 'image'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<Jokes
|
||||
v-else-if="widgetType === 'joke'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<MullvadStatus
|
||||
v-else-if="widgetType === 'mullvad-status'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NdCpuHistory
|
||||
v-else-if="widgetType === 'nd-cpu-history'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NdLoadHistory
|
||||
v-else-if="widgetType === 'nd-load-history'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NdRamHistory
|
||||
v-else-if="widgetType === 'nd-ram-history'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<NewsHeadlines
|
||||
v-else-if="widgetType === 'news-headlines'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<PiHoleStats
|
||||
v-else-if="widgetType === 'pi-hole-stats'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<PiHoleTopQueries
|
||||
v-else-if="widgetType === 'pi-hole-top-queries'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<PiHoleTraffic
|
||||
v-else-if="widgetType === 'pi-hole-traffic'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<PublicHolidays
|
||||
v-else-if="widgetType === 'public-holidays'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<PublicIp
|
||||
v-else-if="widgetType === 'public-ip'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<RssFeed
|
||||
v-else-if="widgetType === 'rss-feed'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<SportsScores
|
||||
v-else-if="widgetType === 'sports-scores'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<StatPing
|
||||
v-else-if="widgetType === 'stat-ping'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<StockPriceChart
|
||||
v-else-if="widgetType === 'stock-price-chart'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<SynologyDownload
|
||||
v-else-if="widgetType === 'synology-download'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<SystemInfo
|
||||
v-else-if="widgetType === 'system-info'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<TflStatus
|
||||
v-else-if="widgetType === 'tfl-status'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<WalletBalance
|
||||
v-else-if="widgetType === 'wallet-balance'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<Weather
|
||||
v-else-if="widgetType === 'weather'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<WeatherForecast
|
||||
v-else-if="widgetType === 'weather-forecast'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<XkcdComic
|
||||
v-else-if="widgetType === 'xkcd-comic'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<!-- No widget type specified -->
|
||||
<div v-else>{{ handleError('Widget type was not found') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -447,6 +39,74 @@ import UpdateIcon from '@/assets/interface-icons/widget-update.svg';
|
|||
import OpenIcon from '@/assets/interface-icons/open-new-tab.svg';
|
||||
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
|
||||
|
||||
const COMPAT = {
|
||||
'adguard-dns-info': 'AdGuardDnsInfo',
|
||||
'adguard-filter-status': 'AdGuardFilterStatus',
|
||||
'adguard-stats': 'AdGuardStats',
|
||||
'adguard-top-domains': 'AdGuardTopDomains',
|
||||
anonaddy: 'AnonAddy',
|
||||
apod: 'Apod',
|
||||
'blacklist-check': 'BlacklistCheck',
|
||||
clock: 'Clock',
|
||||
'crypto-price-chart': 'CryptoPriceChart',
|
||||
'crypto-watch-list': 'CryptoWatchList',
|
||||
'cve-vulnerabilities': 'CveVulnerabilities',
|
||||
'domain-monitor': 'DomainMonitor',
|
||||
'code-stats': 'CodeStats',
|
||||
'covid-stats': 'CovidStats',
|
||||
embed: 'EmbedWidget',
|
||||
'eth-gas-prices': 'EthGasPrices',
|
||||
'exchange-rates': 'ExchangeRates',
|
||||
'flight-data': 'Flights',
|
||||
'github-profile-stats': 'GitHubProfile',
|
||||
'github-trending-repos': 'GitHubTrending',
|
||||
'gl-alerts': 'GlAlerts',
|
||||
'gl-current-cores': 'GlCpuCores',
|
||||
'gl-current-cpu': 'GlCpuGauge',
|
||||
'gl-cpu-history': 'GlCpuHistory',
|
||||
'gl-disk-io': 'GlDiskIo',
|
||||
'gl-disk-space': 'GlDiskSpace',
|
||||
'gl-ip-address': 'GlIpAddress',
|
||||
'gl-load-history': 'GlLoadHistory',
|
||||
'gl-current-mem': 'GlMemGauge',
|
||||
'gl-mem-history': 'GlMemHistory',
|
||||
'gl-network-interfaces': 'GlNetworkInterfaces',
|
||||
'gl-network-traffic': 'GlNetworkTraffic',
|
||||
'gl-system-load': 'GlSystemLoad',
|
||||
'gl-cpu-temp': 'GlCpuTemp',
|
||||
'health-checks': 'HealthChecks',
|
||||
iframe: 'IframeWidget',
|
||||
image: 'ImageWidget',
|
||||
joke: 'Jokes',
|
||||
'mullvad-status': 'MullvadStatus',
|
||||
'nd-cpu-history': 'NdCpuHistory',
|
||||
'nd-load-history': 'NdLoadHistory',
|
||||
'nd-ram-history': 'NdRamHistory',
|
||||
'news-headlines': 'NewsHeadlines',
|
||||
'nextcloud-notifications': 'NextcloudNotifications',
|
||||
'nextcloud-php-opcache': 'NextcloudPhpOpcache',
|
||||
'nextcloud-stats': 'NextcloudStats',
|
||||
'nextcloud-system': 'NextcloudSystem',
|
||||
'nextcloud-user': 'NextcloudUser',
|
||||
'nextcloud-user-status': 'NextcloudUserStatus',
|
||||
'pi-hole-stats': 'PiHoleStats',
|
||||
'pi-hole-top-queries': 'PiHoleTopQueries',
|
||||
'pi-hole-traffic': 'PiHoleTraffic',
|
||||
'public-holidays': 'PublicHolidays',
|
||||
'public-ip': 'PublicIp',
|
||||
'rss-feed': 'RssFeed',
|
||||
'sports-scores': 'SportsScores',
|
||||
'stat-ping': 'StatPing',
|
||||
'stock-price-chart': 'StockPriceChart',
|
||||
'synology-download': 'SynologyDownload',
|
||||
'system-info': 'SystemInfo',
|
||||
'tfl-status': 'TflStatus',
|
||||
'wallet-balance': 'WalletBalance',
|
||||
weather: 'Weather',
|
||||
'weather-forecast': 'WeatherForecast',
|
||||
'xkcd-comic': 'XkcdComic',
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'Widget',
|
||||
components: {
|
||||
|
@ -455,66 +115,6 @@ export default {
|
|||
UpdateIcon,
|
||||
OpenIcon,
|
||||
LoadingAnimation,
|
||||
// Register widget components
|
||||
AdGuardDnsInfo: () => import('@/components/Widgets/AdGuardDnsInfo.vue'),
|
||||
AdGuardFilterStatus: () => import('@/components/Widgets/AdGuardFilterStatus.vue'),
|
||||
AdGuardStats: () => import('@/components/Widgets/AdGuardStats.vue'),
|
||||
AdGuardTopDomains: () => import('@/components/Widgets/AdGuardTopDomains.vue'),
|
||||
AnonAddy: () => import('@/components/Widgets/AnonAddy.vue'),
|
||||
Apod: () => import('@/components/Widgets/Apod.vue'),
|
||||
BlacklistCheck: () => import('@/components/Widgets/BlacklistCheck.vue'),
|
||||
Clock: () => import('@/components/Widgets/Clock.vue'),
|
||||
CodeStats: () => import('@/components/Widgets/CodeStats.vue'),
|
||||
CovidStats: () => import('@/components/Widgets/CovidStats.vue'),
|
||||
CryptoPriceChart: () => import('@/components/Widgets/CryptoPriceChart.vue'),
|
||||
CryptoWatchList: () => import('@/components/Widgets/CryptoWatchList.vue'),
|
||||
CveVulnerabilities: () => import('@/components/Widgets/CveVulnerabilities.vue'),
|
||||
DomainMonitor: () => import('@/components/Widgets/DomainMonitor.vue'),
|
||||
EmbedWidget: () => import('@/components/Widgets/EmbedWidget.vue'),
|
||||
EthGasPrices: () => import('@/components/Widgets/EthGasPrices.vue'),
|
||||
ExchangeRates: () => import('@/components/Widgets/ExchangeRates.vue'),
|
||||
Flights: () => import('@/components/Widgets/Flights.vue'),
|
||||
GitHubTrending: () => import('@/components/Widgets/GitHubTrending.vue'),
|
||||
GitHubProfile: () => import('@/components/Widgets/GitHubProfile.vue'),
|
||||
GlAlerts: () => import('@/components/Widgets/GlAlerts.vue'),
|
||||
GlCpuCores: () => import('@/components/Widgets/GlCpuCores.vue'),
|
||||
GlCpuGauge: () => import('@/components/Widgets/GlCpuGauge.vue'),
|
||||
GlCpuHistory: () => import('@/components/Widgets/GlCpuHistory.vue'),
|
||||
GlDiskIo: () => import('@/components/Widgets/GlDiskIo.vue'),
|
||||
GlDiskSpace: () => import('@/components/Widgets/GlDiskSpace.vue'),
|
||||
GlIpAddress: () => import('@/components/Widgets/GlIpAddress.vue'),
|
||||
GlLoadHistory: () => import('@/components/Widgets/GlLoadHistory.vue'),
|
||||
GlMemGauge: () => import('@/components/Widgets/GlMemGauge.vue'),
|
||||
GlMemHistory: () => import('@/components/Widgets/GlMemHistory.vue'),
|
||||
GlNetworkInterfaces: () => import('@/components/Widgets/GlNetworkInterfaces.vue'),
|
||||
GlNetworkTraffic: () => import('@/components/Widgets/GlNetworkTraffic.vue'),
|
||||
GlSystemLoad: () => import('@/components/Widgets/GlSystemLoad.vue'),
|
||||
GlCpuTemp: () => import('@/components/Widgets/GlCpuTemp.vue'),
|
||||
HealthChecks: () => import('@/components/Widgets/HealthChecks.vue'),
|
||||
IframeWidget: () => import('@/components/Widgets/IframeWidget.vue'),
|
||||
ImageWidget: () => import('@/components/Widgets/ImageWidget.vue'),
|
||||
Jokes: () => import('@/components/Widgets/Jokes.vue'),
|
||||
MullvadStatus: () => import('@/components/Widgets/MullvadStatus.vue'),
|
||||
NdCpuHistory: () => import('@/components/Widgets/NdCpuHistory.vue'),
|
||||
NdLoadHistory: () => import('@/components/Widgets/NdLoadHistory.vue'),
|
||||
NdRamHistory: () => import('@/components/Widgets/NdRamHistory.vue'),
|
||||
NewsHeadlines: () => import('@/components/Widgets/NewsHeadlines.vue'),
|
||||
PiHoleStats: () => import('@/components/Widgets/PiHoleStats.vue'),
|
||||
PiHoleTopQueries: () => import('@/components/Widgets/PiHoleTopQueries.vue'),
|
||||
PiHoleTraffic: () => import('@/components/Widgets/PiHoleTraffic.vue'),
|
||||
PublicHolidays: () => import('@/components/Widgets/PublicHolidays.vue'),
|
||||
PublicIp: () => import('@/components/Widgets/PublicIp.vue'),
|
||||
RssFeed: () => import('@/components/Widgets/RssFeed.vue'),
|
||||
SportsScores: () => import('@/components/Widgets/SportsScores.vue'),
|
||||
StatPing: () => import('@/components/Widgets/StatPing.vue'),
|
||||
StockPriceChart: () => import('@/components/Widgets/StockPriceChart.vue'),
|
||||
SynologyDownload: () => import('@/components/Widgets/SynologyDownload.vue'),
|
||||
SystemInfo: () => import('@/components/Widgets/SystemInfo.vue'),
|
||||
TflStatus: () => import('@/components/Widgets/TflStatus.vue'),
|
||||
WalletBalance: () => import('@/components/Widgets/WalletBalance.vue'),
|
||||
Weather: () => import('@/components/Widgets/Weather.vue'),
|
||||
WeatherForecast: () => import('@/components/Widgets/WeatherForecast.vue'),
|
||||
XkcdComic: () => import('@/components/Widgets/XkcdComic.vue'),
|
||||
},
|
||||
props: {
|
||||
widget: Object,
|
||||
|
@ -556,6 +156,15 @@ export default {
|
|||
hideControls() {
|
||||
return this.widget.hideControls;
|
||||
},
|
||||
component() {
|
||||
const type = COMPAT[this.widgetType] || this.widget.type;
|
||||
if (!type) {
|
||||
ErrorHandler('Widget type was not found');
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line prefer-template
|
||||
return () => import('@/components/Widgets/' + type + '.vue').catch(() => import('@/components/Widgets/Blank.vue'));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Calls update data method on widget */
|
||||
|
|
|
@ -22,6 +22,7 @@ export default {
|
|||
return {
|
||||
statusResponse: undefined,
|
||||
contextMenuOpen: false,
|
||||
intervalId: undefined, // status-check setInterval() id
|
||||
contextPos: {
|
||||
posX: undefined,
|
||||
posY: undefined,
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
15
src/store.js
15
src/store.js
|
@ -10,6 +10,7 @@ import { applyItemId } from '@/utils/SectionHelpers';
|
|||
import filterUserSections from '@/utils/CheckSectionVisibility';
|
||||
import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||
import { isUserAdmin } from '@/utils/Auth';
|
||||
import { localStorageKeys } from './utils/defaults';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
@ -295,6 +296,11 @@ const store = new Vuex.Store({
|
|||
state.navigateConfToTab = index;
|
||||
},
|
||||
[SET_CURRENT_SUB_PAGE](state, subPageObject) {
|
||||
if (!subPageObject) {
|
||||
// Set theme back to primary when navigating to index page
|
||||
const defaulTheme = localStorage.getItem(localStorageKeys.PRIMARY_THEME);
|
||||
if (defaulTheme) state.config.appConfig.theme = defaulTheme;
|
||||
}
|
||||
state.currentConfigInfo = subPageObject;
|
||||
},
|
||||
[USE_MAIN_CONFIG](state) {
|
||||
|
@ -312,13 +318,22 @@ const store = new Vuex.Store({
|
|||
commit(SET_REMOTE_CONFIG, yaml.load((await axios.get('/conf.yml')).data));
|
||||
const deepCopy = (json) => JSON.parse(JSON.stringify(json));
|
||||
const config = deepCopy(new ConfigAccumulator().config());
|
||||
if (config.appConfig?.theme) {
|
||||
// Save theme defined in conf.yml as primary
|
||||
localStorage.setItem(localStorageKeys.PRIMARY_THEME, config.appConfig.theme);
|
||||
// This will set theme back to primary in case we were on a themed page
|
||||
// and the index page is loaded w/o navigation (e.g. modifying browser location)
|
||||
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
|
||||
}
|
||||
commit(SET_CONFIG, config);
|
||||
},
|
||||
/* Fetch config for a sub-page (sections and pageInfo only) */
|
||||
async [INITIALIZE_MULTI_PAGE_CONFIG]({ commit, state }, configPath) {
|
||||
axios.get(configPath).then((response) => {
|
||||
const subConfig = yaml.load(response.data);
|
||||
const pageTheme = subConfig.appConfig?.theme;
|
||||
subConfig.appConfig = state.config.appConfig; // Always use parent appConfig
|
||||
if (pageTheme) subConfig.appConfig.theme = pageTheme; // Apply page theme override
|
||||
commit(SET_CONFIG, subConfig);
|
||||
}).catch((err) => {
|
||||
ErrorHandler(`Unable to load config from '${configPath}'`, err);
|
||||
|
|
|
@ -1542,8 +1542,6 @@ html[data-theme="oblivion-scotch"] {
|
|||
--primary: #d69e3a;
|
||||
}
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Shrikhand&display=swap');
|
||||
|
||||
html[data-theme='lissy'] {
|
||||
// --primary: #f0f;
|
||||
--primary: #ffffffcc;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -120,6 +120,7 @@ module.exports = {
|
|||
COLLAPSE_STATE: 'collapseState',
|
||||
ICON_SIZE: 'iconSize',
|
||||
THEME: 'theme',
|
||||
PRIMARY_THEME: 'primaryTheme',
|
||||
CUSTOM_COLORS: 'customColors',
|
||||
CONF_SECTIONS: 'confSections',
|
||||
CONF_WIDGETS: 'confSections',
|
||||
|
@ -217,7 +218,7 @@ module.exports = {
|
|||
/* API endpoints for widgets that need to fetch external data */
|
||||
widgetApiEndpoints: {
|
||||
anonAddy: 'https://app.anonaddy.com',
|
||||
astronomyPictureOfTheDay: 'https://apodapi.herokuapp.com/api',
|
||||
astronomyPictureOfTheDay: 'https://go-apod.herokuapp.com/apod',
|
||||
blacklistCheck: 'https://api.blacklistchecker.com/check',
|
||||
codeStats: 'https://codestats.net/',
|
||||
covidStats: 'https://disease.sh/v3/covid-19',
|
||||
|
@ -242,7 +243,7 @@ module.exports = {
|
|||
rssToJson: 'https://api.rss2json.com/v1/api.json',
|
||||
sportsScores: 'https://www.thesportsdb.com/api/v1/json',
|
||||
stockPriceChart: 'https://www.alphavantage.co/query',
|
||||
tflStatus: 'https://api.tfl.gov.uk/line/mode/tube/status',
|
||||
tflStatus: 'https://api.tfl.gov.uk/line/mode/dlr,elizabeth-line,overground,tram,tube/status',
|
||||
walletBalance: 'https://api.blockcypher.com/v1',
|
||||
walletQrCode: 'https://www.bitcoinqrcodemaker.com/api',
|
||||
weather: 'https://api.openweathermap.org/data/2.5/weather',
|
||||
|
|
Loading…
Reference in New Issue