Skip to content

Commit e451f86

Browse files
authoredJun 4, 2022
🔀 Merge pull request #685 from Lissy93/FEATURE/minor-improvments-2.1.0
[FEATURE] AdGuard Widget and QoL Improvments Closes #493 Closes #669 Closes #680 Closes #681 Closes #682 Closes #688
2 parents ef786db + 9575090 commit e451f86

23 files changed

+11785
-11149
lines changed
 

‎.github/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## ✨ 2.0.9 Adds Multi-Page Support [PR #685](https://github.com/Lissy93/dashy/pull/685)
4+
- Adds Widgets for AdGuard
5+
36
## ✨ 2.0.9 Adds Multi-Page Support [PR #663](https://github.com/Lissy93/dashy/pull/663)
47
- Fix KeyCloak API URL (#564)
58
- Fix guest has config access (#590)

‎.github/LATEST_CHANGELOG.md

+2-17
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,2 @@
1-
## ✨ 2.0.9 Adds Multi-Page Support [PR #663](https://github.com/Lissy93/dashy/pull/663)
2-
- Fix KeyCloak API URL (#564)
3-
- Fix guest has config access (#590)
4-
- Fix collapsible content in multi-page support (#626)
5-
- Fix layout and item size buttons ( #629)
6-
- Refactor make request in RSS widget (#632)
7-
- Fix material-design-icons header in schema (#640)
8-
- Add option to hide seconds in clock widget (#644)
9-
- Fix pageInfo not being read in router (#645)
10-
- Fix startingView not honored (#646)
11-
- Fix Status Check default (#651)
12-
- Add option to hide image in SportsScores Widget (#654)
13-
- Add Adventure-basic theme (#655)
14-
- Write docs for sub-items (#657)
15-
- Add Font-Awesome displaying as square to troubleshooting guide (#659)
16-
- Show expand / collapse in context menu (#660)
17-
- Only deploy new release when relevant files have changed
1+
## ✨ 2.0.9 Adds Multi-Page Support [PR #685](https://github.com/Lissy93/dashy/pull/685)
2+
- Adds Widgets for AdGuard

‎docs/privacy.md

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ Dashy supports [Widgets](/docs/widgets.md) for displaying dynamic content. Below
111111
- [IP-API Privacy Policy](https://ip-api.com/docs/legal)
112112
- **[IP Blacklist](/docs/widgets.md#ip-blacklist)**: `https://api.blacklistchecker.com`
113113
- [Blacklist Checker Privacy Policy](https://blacklistchecker.com/privacy)
114+
- **[Domain Monitor](/docs/widgets.md#domain-monitor)**: `http://api.whoapi.com`
115+
- [WhoAPI Privacy Policy](https://whoapi.com/privacy-policy/)
114116
- **[Crypto Watch List](/docs/widgets.md#crypto-watch-list)** and **[Token Price History](/docs/widgets.md#crypto-token-price-history)**: `https://api.coingecko.com`
115117
- [CoinGecko Privacy Policy](https://www.coingecko.com/en/privacy)
116118
- **[Wallet Balance](/docs/widgets.md#wallet-balance)**: `https://api.blockcypher.com/`

‎docs/showcase.md

+48-40
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,78 @@
1010
---
1111

1212
### Ratty222
13-
> By [@ratty222](https://github.com/ratty222) <sup>[#384](https://github.com/Lissy93/dashy/discussions/384)</sup>
13+
> By [@ratty222](https://github.com/ratty222) <sup>Re: [#384](https://github.com/Lissy93/dashy/discussions/384)</sup>
1414
1515
![screenshot-ratty222-dashy](https://user-images.githubusercontent.com/1862727/147582551-4c655d37-8bcc-4f95-ab41-164a9d0d6a07.png)
1616

1717
---
1818

1919
### Hugalafutro Dashy
20-
> By [@hugalafutro](https://github.com/hugalafutro) <sup>[#505](https://github.com/Lissy93/dashy/discussions/505)</sup>
20+
> By [@hugalafutro](https://github.com/hugalafutro) <sup>Re: [#505](https://github.com/Lissy93/dashy/discussions/505)</sup>
2121
2222
[![hugalafutro-dashy-screenshot](https://i.ibb.co/PDpLDKS/hugalafutro-dashy.gif)](https://i.ibb.co/PDpLDKS/hugalafutro-dashy.gif)
2323

2424
---
2525

26-
### Networking Services
27-
> By [@Lissy93](https://github.com/lissy93)
26+
### NAS Home Dashboard
27+
> By [@cerealconyogurt](https://github.com/cerealconyogurt) <sup>Re: [#74](https://github.com/Lissy93/dashy/issues/74)</sup>
2828
29-
![screenshot-networking-services](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/2-networking-services-minimal-dark.png)
29+
![screenshot-networking-services](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/6-nas-home-dashboard.png)
30+
31+
---
32+
33+
### Brewhack
34+
35+
> By [@brpeterso](https://github.com/brpeterso) <sup>Re: [#680](https://github.com/Lissy93/dashy/issues/680)</sup>
36+
37+
![screenshot-brewhack-dashboard](https://i.ibb.co/cNjzPT4/brewhack.png)
38+
39+
---
40+
41+
### The Private Dashboard
42+
43+
> By [@DylanBeMe](https://github.com/DylanBeMe) <sup>Re: [#419](https://github.com/Lissy93/dashy/issues/419)</sup>
44+
45+
![screenshot-private-dashboard](https://i.ibb.co/hKS483T/private-dashboard-Dylan-Be-Me.png)
3046

3147
---
3248

3349
### Homelab & VPS dashboard
34-
> By [@shadowking001](https://github.com/shadowking001)
50+
> By [@shadowking001](https://github.com/shadowking001) <sup>Re: [#86](https://github.com/Lissy93/dashy/issues/86)</sup>
3551
3652
![screenshot-shadowking001-dashy](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/8-shadowking001s-dashy.png)
3753

3854
---
3955

40-
### EVO Dashboard
56+
### Raspberry PI Docker Dashboard
4157

42-
> By [@EVOTk](https://github.com/EVOTk)
58+
> By [@henkiewie](https://github.com/henkiewie) <sup>Re: [#622](https://github.com/Lissy93/dashy/issues/622)</sup>
4359
44-
![screenshot-evo-dashboard](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/12-evo-dashboard.png)
60+
> I use this dashboard every day. It now even includes a player for a radio stream which I configured with Logitech media server and icecast. I made an smaller version of the grafana dashboard to fit an iframe in kiosk mode, so it monitors the most important values of my RPI. The PI is in Argon m2 case and used as a NAS. The dashboard is a copy of the adventure theme with some changes saved in `/app/src/styles/user-defined-themes.scss`
61+
62+
![screenshot-henkiewie-dashy](https://i.ibb.co/jGzPm6b/henkiewie-dashy-showcase.png)
4563

4664
---
4765

48-
### The Private Dashboard
66+
### First Week of Self-Hosting
67+
> By [u//RickyCZ](https://www.reddit.com/user/RickyCZ) <sup>via [Reddit](https://www.reddit.com/r/selfhosted/comments/pose15/just_got_started_a_week_ago_selfhosting_is_very/)</sup>
4968
50-
> By [@DylanBeMe](https://github.com/DylanBeMe) <sup>[#419](https://github.com/Lissy93/dashy/issues/419)</sup>
69+
![screenshot-week-of-self-hosting](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/11-ricky-cz.png)
70+
71+
---
72+
73+
### EVO Dashboard
74+
75+
> By [@EVOTk](https://github.com/EVOTk) <sup>Re: [#316](https://github.com/Lissy93/dashy/pull/316)</sup>
5176
52-
![screenshot-evo-dashboard](https://i.ibb.co/hKS483T/private-dashboard-Dylan-Be-Me.png)
77+
![screenshot-evo-dashboard](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/12-evo-dashboard.png)
5378

5479
---
5580

56-
### NAS Home Dashboard
57-
> By [@cerealconyogurt](https://github.com/cerealconyogurt)
81+
### Networking Services
82+
> By [@Lissy93](https://github.com/lissy93)
5883
59-
![screenshot-networking-services](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/6-nas-home-dashboard.png)
84+
![screenshot-networking-services](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/2-networking-services-minimal-dark.png)
6085

6186
---
6287

@@ -108,16 +133,9 @@
108133

109134
---
110135

111-
### First Week of Self-Hosting
112-
> By [u//RickyCZ](https://www.reddit.com/user/RickyCZ)
113-
114-
![screenshot-week-of-self-hosting](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/11-ricky-cz.png)
115-
116-
---
117-
118136
### HomeLAb 3.0
119137

120-
> By [@skoogee](https://github.com/skoogee) (http://zhrn.cc)
138+
> By [@skoogee](https://github.com/skoogee) (http://zhrn.cc) <sup>[#279](https://github.com/Lissy93/dashy/issues/279)</sup>
121139
122140
> Dashy, is the most complete dashboard I ever tried, has all the features, and it sets itself apart from the rest. It is my default homepage now. I am thankful to the developer @Lissy93 for sharing such a wonderful creation.
123141
@@ -126,19 +144,12 @@
126144
---
127145

128146
### Ground Control
129-
> By [@dtctek](https://github.com/dtctek)
147+
> By [@dtctek](https://github.com/dtctek) <sup>Re: [#83](https://github.com/Lissy93/dashy/issues/83)</sup>
130148
131149
![screenshot-ground-control](https://user-images.githubusercontent.com/1862727/149821995-e9b41dab-186c-42e6-b5b3-e233259b241d.png)
132150

133151
---
134152

135-
### Morning Dashboard
136-
> Displayed on my smart screen between 05:00 - 08:00, and includes all the info that I usually check before leaving for work
137-
138-
![screenshot-morning-dash](https://i.ibb.co/4Wx8zb7/morning-dashboard.png)
139-
140-
---
141-
142153
### Croco_Grievous
143154
> By [u/Croco_Grievous](https://www.reddit.com/user/Croco_Grievous/) <sup>via [reddit](https://www.reddit.com/r/selfhosted/comments/t4xk3z/everything_started_with_pihole_on_a_raspberry_pi/)</sup>
144155
@@ -154,20 +165,17 @@
154165

155166
---
156167

157-
### Raspberry PI Docker Dashboard
158-
159-
> By [@henkiewie](https://github.com/henkiewie) <sup>via [#622](https://github.com/Lissy93/dashy/issues/622)</sup>
160-
161-
> I use this dashboard every day. It now even includes a player for a radio stream which I configured with Logitech media server and icecast. I made an smaller version of the grafana dashboard to fit an iframe in kiosk mode, so it monitors the most important values of my RPI. The PI is in Argon m2 case and used as a NAS. The dashboard is a copy of the adventure theme with some changes saved in `/app/src/styles/user-defined-themes.scss`
168+
### Stefantigro
169+
> By [u/stefantigro](https://www.reddit.com/user/stefantigro/) <sup>via [reddit](https://www.reddit.com/r/selfhosted/comments/t5oril/been_selfhosting_close_to_half_a_year_now_all/)</sup>
162170
163-
![screenshot-henkiewie-dashy](https://i.ibb.co/jGzPm6b/henkiewie-dashy-showcase.png)
171+
![screenshot-stefantigro-dashy](https://i.ibb.co/1Kb43Yy/dashy-stefantigro.png)
164172

165173
---
166174

167-
### Stefantigro
168-
> By [u/stefantigro](https://www.reddit.com/user/stefantigro/) <sup>via [reddit](https://www.reddit.com/r/selfhosted/comments/t5oril/been_selfhosting_close_to_half_a_year_now_all/)</sup>
175+
### Morning Dashboard
176+
> Displayed on my smart screen between 05:00 - 08:00, and includes all the info that I usually check before leaving for work
169177
170-
![screenshot-stefantigro-dashy](https://i.ibb.co/1Kb43Yy/dashy-stefantigro.png)
178+
![screenshot-morning-dash](https://i.ibb.co/4Wx8zb7/morning-dashboard.png)
171179

172180
---
173181

‎docs/troubleshooting.md

+12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- [Fixing Widget CORS Errors](#widget-cors-errors)
3232
- [Weather Forecast Widget 401](#weather-forecast-widget-401)
3333
- [Font Awesome Icons not Displaying](#font-awesome-icons-not-displaying)
34+
- [Copy to Clipboard not Working](#copy-to-clipboard-not-working)
3435
- [How-To Open Browser Console](#how-to-open-browser-console)
3536
- [Git Contributions not Displaying](#git-contributions-not-displaying)
3637

@@ -436,6 +437,17 @@ Finally, check the [browser console](#how-to-open-browser-console) for any error
436437
437438
---
438439
440+
## Copy to Clipboard not Working
441+
442+
If the copy to clipboard feature (either under Config --> Export, or Item --> Copy URL) isn't functioning as expected, first check the browser console. If you see `TypeError: Cannot read properties of undefined (reading 'writeText')` then this feature is not supported by your browser.
443+
The most common reason for this, is if you not running the app over HTTPS. Copying to the clipboard requires the app to be running in a secure origin / aka have valid HTTPS cert. You can read more about this [here](https://stackoverflow.com/a/71876238/979052).
444+
445+
As a workaround, you could either:
446+
- Highlight the text and copy / <kbd>Ctrl</kbd> + <kbd>C</kbd>
447+
- Or setup SSL - [here's a guide](https://github.com/Lissy93/dashy/blob/master/docs/management.md#ssl-certificates) on doing so
448+
449+
---
450+
439451
## How-To Open Browser Console
440452
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.
441453

‎docs/widgets.md

+189-5
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
Dashy has support for displaying dynamic content in the form of widgets. There are several built-in widgets available out-of-the-box as well as support for custom widgets to display stats from almost any service with an API.
44

5-
> ℹ️ **Note**: Widgets are still in the Alpha-phase of development.
6-
> If you find a bug, please raise it.<br>
7-
> Adding / editing widgets through the UI isn't yet supported, you will need to do this in the YAML config file.
8-
95
##### Contents
106
- **[General Widgets](#general-widgets)**
117
- [Clock](#clock)
@@ -15,6 +11,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
1511
- [Image](#image)
1612
- [Public IP Address](#public-ip)
1713
- [IP Blacklist Checker](#ip-blacklist)
14+
- [Domain Monitor](#domain-monitor)
1815
- [Crypto Watch List](#crypto-watch-list)
1916
- [Crypto Price History](#crypto-token-price-history)
2017
- [Crypto Wallet Balance](#wallet-balance)
@@ -47,6 +44,10 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
4744
- [Recent Traffic](#recent-traffic)
4845
- [Stat Ping Statuses](#stat-ping-statuses)
4946
- [Synology Download Station](#synology-download-station)
47+
- [AdGuard Home Block Stats](#adguard-home-block-stats)
48+
- [AdGuard Home Filters](#adguard-home-filters)
49+
- [AdGuard Home DNS Info](#adguard-home-dns-info)
50+
- [AdGuard Home Top Domains](#adguard-home-top-domains)
5051
- **[System Resource Monitoring](#system-resource-monitoring)**
5152
- [CPU Usage Current](#current-cpu-usage)
5253
- [CPU Usage Per Core](#cpu-usage-per-core)
@@ -320,6 +321,43 @@ Notice certain web pages aren't loading? This widget quickly shows which blackli
320321

321322
---
322323

324+
### Domain Monitor
325+
326+
Keep an eye on the expiry dates of your domain names, using public whois records fetched from [whoapi.com](https://whoapi.com/). Click the domain name to view additional info, like registrar, name servers and date last updated.
327+
328+
<p align="center"><img width="600" src="https://i.ibb.co/7XjByG9/domain-monitor.png" /></p>
329+
330+
##### Options
331+
332+
**Field** | **Type** | **Required** | **Description**
333+
--- | --- | --- | ---
334+
**`domain`** | `string` | Required | The domain to check
335+
**`apiKey`** | `string` | Required | You can get your free API key from [my.whoapi.com](https://my.whoapi.com/user/signup)
336+
**`showFullInfo`** | `boolean` | _Optional_ | If set to true, the toggle-full-info panel will be open by default
337+
338+
##### Example
339+
340+
```yaml
341+
- type: domain-monitor
342+
options:
343+
domain: example.com
344+
apiKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
345+
346+
- type: domain-monitor
347+
options:
348+
domain: example2.com
349+
apiKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
350+
```
351+
352+
##### Info
353+
- **CORS**: 🟢 Enabled
354+
- **Auth**: 🔴 Required
355+
- **Price**: 🟠 Free Plan (10,000 requests)
356+
- **Host**: Managed Instance Only
357+
- **Privacy**: _See [WhoAPI Privacy Policy](https://whoapi.com/privacy-policy/)_
358+
359+
---
360+
323361
### Crypto Watch List
324362

325363
Keep track of price changes of your favorite crypto assets. Data is fetched from [CoinGecko](https://www.coingecko.com/). All fields are optional.
@@ -1334,7 +1372,7 @@ Displays the current and recent uptime of your running services, via a self-host
13341372

13351373
Displays the current downloads/torrents tasks of your Synology NAS
13361374

1337-
<p align="center"><img width="300" src="https://i.ibb.co/N2kKWTN/image.png" /></p>
1375+
<p align="center"><img width="500" src="https://i.ibb.co/N2kKWTN/image.png" /></p>
13381376

13391377
##### Options
13401378

@@ -1365,6 +1403,152 @@ Displays the current downloads/torrents tasks of your Synology NAS
13651403

13661404
---
13671405

1406+
### AdGuard Home Block Stats
1407+
1408+
Fetches data from your [AdGuard Home](https://adguard.com/en/adguard-home/overview.html) instance, and
1409+
displays total number of allowed and blocked queries, plus a pie chart showing breakdown by block type.
1410+
1411+
<p align="center"><img width="400" src="https://i.ibb.co/qgkcxsN/adguard-block-percent-2.png" /></p>
1412+
1413+
##### Options
1414+
1415+
**Field** | **Type** | **Required** | **Description**
1416+
--- | --- | --- | ---
1417+
**`hostname`** | `string` | Required | The URL to your AdGuard Home instance
1418+
**`username`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your username here
1419+
**`password`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your password here
1420+
1421+
##### Example
1422+
1423+
```yaml
1424+
- type: adguard-stats
1425+
useProxy: true
1426+
options:
1427+
hostname: http://127.0.0.1
1428+
username: admin
1429+
password: test
1430+
```
1431+
1432+
##### Info
1433+
- **CORS**: 🟠 Proxied
1434+
- **Auth**: 🟠 Optional
1435+
- **Price**: 🟢 Free
1436+
- **Host**: Self-Hosted (see [AdGuard Home](https://adguard.com/en/adguard-home/overview.html))
1437+
- **Privacy**: _See [AdGuard Privacy Policy](https://adguard.com/en/privacy.html)_
1438+
1439+
1440+
---
1441+
1442+
### AdGuard Home Filters
1443+
1444+
Fetches data from your [AdGuard Home](https://adguard.com/en/adguard-home/overview.html) instance, to display the current status of each of your filter lists. Includes filter name, last updated, number of items, and a link to the list.
1445+
1446+
<p align="center"><img width="400" src="https://i.ibb.co/WsJkf5g/adguard-filters-list.png" /></p>
1447+
1448+
##### Options
1449+
1450+
**Field** | **Type** | **Required** | **Description**
1451+
--- | --- | --- | ---
1452+
**`hostname`** | `string` | Required | The URL to your AdGuard Home instance
1453+
**`username`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your username here
1454+
**`password`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your password here
1455+
**`showOnOffStatusOnly`** | `boolean` | _Optional_ | If set to `true`, will only show aggregated AdGuard filter status (on/off), instead of a list of filters
1456+
1457+
##### Example
1458+
1459+
```yaml
1460+
- type: adguard-filter-status
1461+
useProxy: true
1462+
options:
1463+
hostname: http://127.0.0.1
1464+
username: admin
1465+
password: test
1466+
showOnOffStatusOnly: false
1467+
```
1468+
1469+
##### Info
1470+
- **CORS**: 🟠 Proxied
1471+
- **Auth**: 🟠 Optional
1472+
- **Price**: 🟢 Free
1473+
- **Host**: Self-Hosted (see [AdGuard Home](https://adguard.com/en/adguard-home/overview.html))
1474+
- **Privacy**: _See [AdGuard Privacy Policy](https://adguard.com/en/privacy.html)_
1475+
1476+
---
1477+
1478+
### AdGuard Home DNS Info
1479+
1480+
Fetches data from your [AdGuard Home](https://adguard.com/en/adguard-home/overview.html) instance, and displays the current status (Enabled / Disabled) of AdGuard DNS. Click show more to view detailed info, including upstream DNS provider, active ports, and the status of DNSSEC, EDNS CS, PTR and IPv6.
1481+
1482+
<p align="center"><img width="400" src="https://i.ibb.co/G0JngBb/adguard-dns-info.png" /></p>
1483+
1484+
##### Options
1485+
1486+
**Field** | **Type** | **Required** | **Description**
1487+
--- | --- | --- | ---
1488+
**`hostname`** | `string` | Required | The URL to your AdGuard Home instance
1489+
**`username`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your username here
1490+
**`password`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your password here
1491+
**`showFullInfo`** | `boolean` | _Optional_ | If set to `true`, the full DNS info will be shown by default, without having to click "Show Info"
1492+
1493+
##### Example
1494+
1495+
```yaml
1496+
- type: adguard-dns-info
1497+
useProxy: true
1498+
options:
1499+
hostname: http://127.0.0.1
1500+
username: admin
1501+
password: test
1502+
showFullInfo: false
1503+
```
1504+
1505+
##### Info
1506+
- **CORS**: 🟠 Proxied
1507+
- **Auth**: 🟠 Optional
1508+
- **Price**: 🟢 Free
1509+
- **Host**: Self-Hosted (see [AdGuard Home](https://adguard.com/en/adguard-home/overview.html))
1510+
- **Privacy**: _See [AdGuard Privacy Policy](https://adguard.com/en/privacy.html)_
1511+
1512+
---
1513+
1514+
### AdGuard Home Top Domains
1515+
1516+
Fetches data from your [AdGuard Home](https://adguard.com/en/adguard-home/overview.html) instance, and displays a list of the most queried, and most blocked domains.
1517+
1518+
<p align="center"><img width="600" src="https://i.ibb.co/qRhYYTk/adguard-top-domains.png" /></p>
1519+
1520+
##### Options
1521+
1522+
**Field** | **Type** | **Required** | **Description**
1523+
--- | --- | --- | ---
1524+
**`hostname`** | `string` | Required | The URL to your AdGuard Home instance
1525+
**`username`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your username here
1526+
**`password`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your password here
1527+
**`limit`** | `number` | _Optional_ | Specify the number of results to show, between `1` and `100`, defaults to `10`
1528+
**`hideBlockedDomains`** | `boolean` | _Optional_ | Don't show the blocked domains list (queried domains only)
1529+
**`hideQueriedDomains`** | `boolean` | _Optional_ | Don't show the queried domains list (blocked domains only)
1530+
1531+
##### Example
1532+
1533+
```yaml
1534+
- type: adguard-top-domains
1535+
useProxy: true
1536+
options:
1537+
hostname: http://127.0.0.1
1538+
username: admin
1539+
password: test
1540+
limit: 10
1541+
```
1542+
1543+
##### Info
1544+
- **CORS**: 🟠 Proxied
1545+
- **Auth**: 🟠 Optional
1546+
- **Price**: 🟢 Free
1547+
- **Host**: Self-Hosted (see [AdGuard Home](https://adguard.com/en/adguard-home/overview.html))
1548+
- **Privacy**: _See [AdGuard Privacy Policy](https://adguard.com/en/privacy.html)_
1549+
1550+
---
1551+
13681552
## System Resource Monitoring
13691553

13701554
The easiest method for displaying system info and resource usage in Dashy is with [Glances](https://nicolargo.github.io/glances/).

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Dashy",
3-
"version": "2.0.9",
3+
"version": "2.1.0",
44
"license": "MIT",
55
"main": "server",
66
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",

‎src/components/InteractiveEditor/ExportConfigMenu.vue

+8-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { modalNames } from '@/utils/defaults';
3838
import AccessError from '@/components/Configuration/AccessError';
3939
import DownloadConfigIcon from '@/assets/interface-icons/config-download-file.svg';
4040
import CopyConfigIcon from '@/assets/interface-icons/interactive-editor-copy-clipboard.svg';
41-
import { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
41+
import { ErrorHandler, InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
4242
4343
export default {
4444
name: 'ExportConfigMenu',
@@ -80,8 +80,13 @@ export default {
8080
},
8181
copyConfigToClipboard() {
8282
const config = this.convertJsonToYaml();
83-
navigator.clipboard.writeText(config);
84-
this.$toasted.show(this.$t('config.data-copied-msg'));
83+
if (navigator.clipboard) {
84+
navigator.clipboard.writeText(config);
85+
this.$toasted.show(this.$t('config.data-copied-msg'));
86+
} else {
87+
ErrorHandler('Clipboard access requires HTTPS. See: https://bit.ly/3N5WuAA');
88+
this.$toasted.show('Unable to copy, see log', { className: 'toast-error' });
89+
}
8590
InfoHandler('Config copied to clipboard', InfoKeys.EDITOR);
8691
},
8792
modalClosed() {
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<template>
2+
<div class="ad-guard-dns-info-wrapper">
3+
<div class="enable-status" v-if="enabled !== null">
4+
<p v-if="enabled" class="status connected"><span>✔</span> Enabled</p>
5+
<p v-else class="status not-connected"><span>✘</span> Disabled</p>
6+
</div>
7+
<p @click="toggleShowData" v-if="dnsInfo.length > 0" class="expend-details-btn">
8+
{{ showData ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
9+
</p>
10+
<div v-if="showData && dnsInfo.length > 0" class="dns-info">
11+
<div v-for="(item, index) in dnsInfo" :key="index" class="row">
12+
<span class="lbl">{{ item.lbl }}: </span>
13+
<span class="val">{{ item.val | renderVal }}</span>
14+
</div>
15+
</div>
16+
</div>
17+
</template>
18+
19+
<script>
20+
import WidgetMixin from '@/mixins/WidgetMixin';
21+
import { capitalize } from '@/utils/MiscHelpers';
22+
23+
export default {
24+
mixins: [WidgetMixin],
25+
computed: {
26+
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
27+
hostname() {
28+
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
29+
return this.options.hostname;
30+
},
31+
showFullInfo() {
32+
return this.options.showFullInfo;
33+
},
34+
endpoint() {
35+
return `${this.hostname}/control/dns_info`;
36+
},
37+
basicInoEndpoint() {
38+
return `${this.hostname}/control/status`;
39+
},
40+
authHeaders() {
41+
if (this.options.username && this.options.password) {
42+
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
43+
return { Authorization: `Basic ${encoded}` };
44+
}
45+
return {};
46+
},
47+
},
48+
data() {
49+
return {
50+
enabled: null,
51+
dnsInfo: [],
52+
showData: false,
53+
};
54+
},
55+
filters: {
56+
renderVal(val) {
57+
if (val === undefined) return 'N/A';
58+
if (Array.isArray(val) && val.length === 0) return 'N/A';
59+
if (typeof val === 'boolean') return val ? '' : '';
60+
if (typeof val === 'string') return capitalize(val);
61+
if (Array.isArray(val)) return val.join('\n');
62+
return val;
63+
},
64+
},
65+
methods: {
66+
/* Make GET request to AdGuard endpoint */
67+
fetchData() {
68+
this.makeRequest(this.basicInoEndpoint, this.authHeaders).then(this.processStatusBasics);
69+
this.makeRequest(this.endpoint, this.authHeaders).then(this.processData);
70+
},
71+
processStatusBasics(data) {
72+
const newInfo = [
73+
{ lbl: 'DNS Address', val: data.dns_addresses },
74+
{ lbl: 'DNS Port', val: data.dns_port },
75+
{ lbl: 'HTTP Port', val: data.http_port },
76+
];
77+
this.dnsInfo = [...this.dnsInfo, ...newInfo];
78+
},
79+
/* Assign data variables to the returned data */
80+
processData(data) {
81+
this.enabled = data.protection_enabled;
82+
const newInfo = [
83+
{ lbl: 'Blocking Mode', val: data.blocking_mode },
84+
{ lbl: 'Cache Size', val: `${data.cache_size} B` },
85+
{ lbl: 'IPv6', val: !data.disable_ipv6 },
86+
{ lbl: 'DNSSEC', val: data.dnssec_enabled },
87+
{ lbl: 'EDNS Client-Subnet', val: data.edns_cs_enabled },
88+
{ lbl: 'Private PTR', val: data.use_private_ptr_resolvers },
89+
{ lbl: 'Upstream DNS', val: data.upstream_dns },
90+
{ lbl: 'PRT Upstream', val: data.local_ptr_upstreams },
91+
{ lbl: 'Bootstrap DNS', val: data.bootstrap_dns },
92+
];
93+
this.dnsInfo = [...this.dnsInfo, ...newInfo];
94+
},
95+
toggleShowData() {
96+
this.showData = !this.showData;
97+
},
98+
},
99+
mounted() {
100+
if (this.showFullInfo) this.showData = true;
101+
},
102+
};
103+
</script>
104+
105+
<style lang="scss">
106+
.ad-guard-dns-info-wrapper {
107+
color: var(--widget-text-color);
108+
.enable-status {
109+
.status {
110+
display: flex;
111+
max-width: 250px;
112+
font-size: 1.5rem;
113+
font-weight: bold;
114+
align-items: center;
115+
margin: 0.25rem auto;
116+
justify-content: space-evenly;
117+
span {
118+
font-size: 1.5rem;
119+
border-radius: 1.5rem;
120+
padding: 0.3rem 0.7rem;
121+
border: 1px solid;
122+
color: var(--background);
123+
}
124+
&.not-connected {
125+
color: var(--danger);
126+
span { background: var(--danger); }
127+
}
128+
&.connected {
129+
color: var(--success);
130+
span { background: var(--success); }
131+
}
132+
}
133+
}
134+
135+
p.expend-details-btn {
136+
cursor: pointer;
137+
text-align: center;
138+
margin: 0;
139+
font-size: 0.9rem;
140+
padding: 0.1rem 0.25rem;
141+
border: 1px solid transparent;
142+
color: var(--widget-text-color);
143+
opacity: var(--dimming-factor);
144+
border-radius: var(--curve-factor);
145+
&:hover {
146+
text-decoration: underline;
147+
}
148+
&:focus, &:active {
149+
background: var(--widget-text-color);
150+
color: var(--widget-background-color);
151+
}
152+
}
153+
}
154+
155+
.dns-info {
156+
.row {
157+
display: flex;
158+
justify-content: space-between;
159+
align-items: center;
160+
padding: 0.2rem 0.1rem;
161+
font-size: 0.9rem;
162+
&:not(:last-child) {
163+
border-bottom: 1px dashed var(--widget-text-color);
164+
}
165+
.val {
166+
max-width: 80%;
167+
overflow: hidden;
168+
white-space: pre;
169+
text-overflow: ellipsis;
170+
font-family: var(--font-monospace);
171+
}
172+
}
173+
}
174+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<template>
2+
<div class="ad-guard-filter-status-wrapper">
3+
<!-- Current Status -->
4+
<div v-if="status !== null && showOnOffStatusOnly" class="status">
5+
<span class="status-lbl">{{ $t('widgets.pi-hole.status-heading') }}:</span>
6+
<span :class="`status-val ${getStatusColor(status)}`">
7+
{{ status ? 'Enabled' : 'Disabled' }}
8+
</span>
9+
</div>
10+
<!-- List of filters -->
11+
<div v-if="filters && !showOnOffStatusOnly" class="filters-list">
12+
<div v-for="filter in filters" :key="filter.id" class="filter">
13+
<!-- Filter status, name and query count -->
14+
<div class="row-1">
15+
<span :class="`on-off ${filter.enabled ? 'green' : 'red'}`">
16+
{{ filter.enabled ? '✔' : '✘' }}
17+
</span>
18+
<span class="filter-name">{{ filter.name }}</span>
19+
<span class="filter-rules-count">{{ filter.rules_count }}</span>
20+
</div>
21+
<!-- Date of last update, and link to list -->
22+
<div class="row-2">
23+
<span class="updated">Updated {{ filter.last_updated | formatDate }}</span>
24+
<a class="filter-link" v-if="filter.url" :href="filter.url">View List</a>
25+
</div>
26+
</div>
27+
</div>
28+
</div>
29+
</template>
30+
31+
<script>
32+
import WidgetMixin from '@/mixins/WidgetMixin';
33+
import { getTimeAgo } from '@/utils/MiscHelpers';
34+
35+
export default {
36+
mixins: [WidgetMixin],
37+
computed: {
38+
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
39+
hostname() {
40+
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
41+
return this.options.hostname;
42+
},
43+
showOnOffStatusOnly() {
44+
return this.options.showOnOffStatusOnly;
45+
},
46+
endpoint() {
47+
return `${this.hostname}/control/filtering/status`;
48+
},
49+
authHeaders() {
50+
if (this.options.username && this.options.password) {
51+
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
52+
return { Authorization: `Basic ${encoded}` };
53+
}
54+
return {};
55+
},
56+
},
57+
data() {
58+
return {
59+
status: null,
60+
filters: null,
61+
};
62+
},
63+
filters: {
64+
formatDate(date) {
65+
if (!date) return 'Never';
66+
return getTimeAgo(date);
67+
},
68+
},
69+
methods: {
70+
/* Make GET request to AdGuard endpoint */
71+
fetchData() {
72+
this.makeRequest(this.endpoint, this.authHeaders).then(this.processData);
73+
},
74+
/* Assign data variables to the returned data */
75+
processData(data) {
76+
this.status = data.enabled;
77+
this.filters = data.filters;
78+
},
79+
getStatusColor(status) {
80+
return status ? 'green' : 'red';
81+
},
82+
},
83+
};
84+
</script>
85+
86+
<style lang="scss">
87+
.ad-guard-filter-status-wrapper {
88+
.status {
89+
margin: 0.5rem 0;
90+
font-size: 1.1rem;
91+
.status-lbl {
92+
color: var(--widget-text-color);
93+
font-weight: bold;
94+
}
95+
.status-val {
96+
font-family: var(--font-monospace);
97+
&.green { color: var(--success); }
98+
&.red { color: var(--danger); }
99+
&.blue { color: var(--info); }
100+
}
101+
}
102+
.filters-list {
103+
.filter {
104+
display: flex;
105+
flex-direction: column;
106+
color: var(--widget-text-color);
107+
padding: 0.25rem 0.1rem;
108+
.row-1 {
109+
display: flex;
110+
justify-content: space-between;
111+
align-items: center;
112+
span.on-off {
113+
margin-right: 0.5rem;
114+
&.green { color: var(--success); }
115+
&.red { color: var(--danger); }
116+
}
117+
span.filter-name {
118+
width: 100%;
119+
overflow: hidden;
120+
white-space: pre;
121+
text-overflow: ellipsis;
122+
}
123+
span.rules_count {
124+
font-family: var(--font-monospace);
125+
}
126+
}
127+
.row-2 {
128+
display: flex;
129+
justify-content: space-between;
130+
span.updated, a.filter-link {
131+
margin: 0.2rem 0;
132+
font-size: 0.8rem;
133+
opacity: var(--dimming-factor);
134+
color: var(--widget-text-color);
135+
}
136+
}
137+
&:not(:last-child) {
138+
border-bottom: 1px dashed var(--widget-text-color);
139+
}
140+
}
141+
}
142+
}
143+
</style>
+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<template>
2+
<div class="ad-guard-stats-wrapper">
3+
<!-- Show total query and block count -->
4+
<div v-if="queryCount && blockCount" class="summary">
5+
<div><span class="lbl">Queries:</span><span class="val">{{ queryCount }}</span></div>
6+
<div><span class="lbl">Blocked:</span><span class="val">{{ blockCount }}</span></div>
7+
</div>
8+
<!-- Pie chart with block breakdown -->
9+
<p :id="chartId" class="block-pie"></p>
10+
</div>
11+
</template>
12+
13+
<script>
14+
import WidgetMixin from '@/mixins/WidgetMixin';
15+
import ChartingMixin from '@/mixins/ChartingMixin';
16+
17+
export default {
18+
mixins: [WidgetMixin, ChartingMixin],
19+
computed: {
20+
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
21+
hostname() {
22+
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
23+
return this.options.hostname;
24+
},
25+
endpoint() {
26+
return `${this.hostname}/control/stats`;
27+
},
28+
authHeaders() {
29+
if (this.options.username && this.options.password) {
30+
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
31+
return { Authorization: `Basic ${encoded}` };
32+
}
33+
return {};
34+
},
35+
},
36+
data() {
37+
return {
38+
queryCount: null,
39+
blockCount: null,
40+
};
41+
},
42+
methods: {
43+
/* Make GET request to AdGuard endpoint */
44+
fetchData() {
45+
this.makeRequest(this.endpoint, this.authHeaders).then(this.processData);
46+
},
47+
/* Assign data variables to the returned data */
48+
processData(data) {
49+
// Get data from response, to be rendered to the chart
50+
const totalAllowed = data.num_dns_queries || 0;
51+
const blocked = data.num_blocked_filtering || 0;
52+
const safeBrowsing = data.num_replaced_safebrowsing || 0;
53+
const safeSearch = data.num_replaced_safesearch || 0;
54+
const parental = data.num_replaced_parental || 0;
55+
const blockTotal = blocked + safeBrowsing + safeSearch + parental;
56+
const remaining = totalAllowed - blockTotal;
57+
58+
// Set query and block count, for first line
59+
this.queryCount = totalAllowed;
60+
this.blockCount = blockTotal;
61+
62+
// Put data into a flat array for the chart
63+
const chartColors = ['#ef476f', '#06d6a0'];
64+
const chartValues = [blocked, remaining];
65+
const chartLabels = ['Blocked', 'Allowed'];
66+
67+
// If additional blocked results are non-zero, append to chart data
68+
if (safeBrowsing > 0) {
69+
chartColors.push('#ffc43d');
70+
chartValues.push(safeBrowsing);
71+
chartLabels.push('Safe Search - Blocked');
72+
}
73+
if (safeSearch > 0) {
74+
chartColors.push('#f8ffe5');
75+
chartValues.push(safeSearch);
76+
chartLabels.push('Safe Search - Blocked');
77+
}
78+
if (parental > 0) {
79+
chartColors.push('#1b9aaa');
80+
chartValues.push(parental);
81+
chartLabels.push('Parental Controls - Blocked');
82+
}
83+
84+
// Call generate chart function
85+
this.generateBlockPie(chartLabels, chartValues, chartColors);
86+
},
87+
/* Generate pie chart showing the proportion of queries blocked */
88+
generateBlockPie(labels, values, colors) {
89+
return new this.Chart(`#${this.chartId}`, {
90+
title: 'AdGuard DNS Queries',
91+
data: {
92+
labels,
93+
datasets: [{ values }],
94+
},
95+
type: 'donut',
96+
height: 250,
97+
strokeWidth: 20,
98+
colors,
99+
tooltipOptions: {
100+
formatTooltipY: d => `${Math.round(d)} queries`,
101+
},
102+
});
103+
},
104+
},
105+
};
106+
</script>
107+
108+
<style lang="scss">
109+
.ad-guard-stats-wrapper {
110+
.block-pie {
111+
margin: 0;
112+
svg.frappe-chart.chart {
113+
overflow: visible;
114+
}
115+
}
116+
.summary {
117+
display: flex;
118+
flex-wrap: wrap;
119+
justify-content: space-around;
120+
color: var(--widget-text-color);
121+
span.lbl {
122+
font-weight: bold;
123+
margin: 0.25rem;
124+
}
125+
span.val {
126+
font-family: var(--font-monospace);
127+
}
128+
}
129+
}
130+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<template>
2+
<div class="ad-guard-top-domains-wrapper">
3+
<!-- List of top blocked domains -->
4+
<div class="sec blocked-domains" v-if="topBlockedDomains && !hideBlockedDomains">
5+
<h3 class="sub-title">Top Blocked Domains</h3>
6+
<div class="row title-row">
7+
<span class="cell domain">Domain</span>
8+
<span class="cell">Query Count</span>
9+
</div>
10+
<div class="row" v-for="(domain, ind) in topBlockedDomains" :key="ind">
11+
<span class="cell domain">{{ domain.name }}</span>
12+
<span class="cell count">{{ domain.count }}</span>
13+
</div>
14+
</div>
15+
<!-- List of top queried domains -->
16+
<div class="sec blocked-domains" v-if="topQueriedDomains && !hideQueriedDomains">
17+
<h3 class="sub-title">Top Queried Domains</h3>
18+
<div class="row title-row">
19+
<span class="cell domain">Domain</span>
20+
<span class="cell">Query Count</span>
21+
</div>
22+
<div class="row" v-for="(domain, ind) in topQueriedDomains" :key="ind">
23+
<span class="cell domain">{{ domain.name }}</span>
24+
<span class="cell count">{{ domain.count }}</span>
25+
</div>
26+
</div>
27+
</div>
28+
</template>
29+
30+
<script>
31+
import WidgetMixin from '@/mixins/WidgetMixin';
32+
33+
export default {
34+
mixins: [WidgetMixin],
35+
computed: {
36+
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
37+
hostname() {
38+
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
39+
return this.options.hostname;
40+
},
41+
authHeaders() {
42+
if (this.options.username && this.options.password) {
43+
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
44+
return { Authorization: `Basic ${encoded}` };
45+
}
46+
return {};
47+
},
48+
limit() {
49+
return this.options.limit || 10;
50+
},
51+
hideBlockedDomains() {
52+
return this.options.hideBlockedDomains;
53+
},
54+
hideQueriedDomains() {
55+
return this.options.hideQueriedDomains;
56+
},
57+
endpoint() {
58+
return `${this.hostname}/control/stats`;
59+
},
60+
},
61+
data() {
62+
return {
63+
topQueriedDomains: null,
64+
topBlockedDomains: null,
65+
};
66+
},
67+
methods: {
68+
/* Make GET request to AdGuard endpoint */
69+
fetchData() {
70+
this.makeRequest(this.endpoint, this.authHeaders).then(this.processData);
71+
},
72+
/* Assign data variables to the returned data */
73+
processData(data) {
74+
this.topQueriedDomains = this.makeDomainData(data.top_queried_domains);
75+
this.topBlockedDomains = this.makeDomainData(data.top_blocked_domains);
76+
},
77+
/* Process AdGruard's weird data format, into something that can be rendered */
78+
makeDomainData(rawData) {
79+
const domains = [];
80+
rawData.forEach((domainBlock) => {
81+
Object.keys(domainBlock).forEach((domain) => {
82+
domains.push({ name: domain, count: domainBlock[domain] });
83+
});
84+
});
85+
return domains.slice(0, this.limit);
86+
},
87+
},
88+
};
89+
</script>
90+
91+
<style lang="scss">
92+
.ad-guard-top-domains-wrapper {
93+
text-align: center;
94+
color: var(--widget-text-color);
95+
.sec {
96+
width: 100%;
97+
max-width: 28rem;
98+
margin-right: 1rem;
99+
display: inline-block;
100+
h3.sub-title {
101+
text-align: left;
102+
font-size: 1.2rem;
103+
margin: 0.4rem 0 0.2rem 0;
104+
}
105+
.row {
106+
display: flex;
107+
font-size: 0.9rem;
108+
align-items: center;
109+
padding: 0.25rem 0.1rem;
110+
justify-content: space-between;
111+
color: var(--widget-text-color);
112+
&:not(:last-child) {
113+
border-bottom: 1px dashed var(--widget-text-color);
114+
}
115+
&.title-row {
116+
font-weight: bold;
117+
border-top: 1px solid var(--widget-text-color);
118+
}
119+
.cell {
120+
&.count {
121+
font-family: var(--font-monospace);
122+
}
123+
}
124+
}
125+
}
126+
}
127+
</style>
+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<template>
2+
<div class="blacklist-check-wrapper">
3+
<!-- Domain Name and Registration State / Expiry Count Down -->
4+
<div class="expiry-wrap" v-if="domainMeta" @click="toggleDomainInfo">
5+
<span class="name">{{ domainMeta.domainName }}</span>
6+
<span v-if="!domainMeta.isRegistered" class="not-registered">
7+
Not Registered
8+
</span>
9+
<span v-if="domainMeta.isRegistered"
10+
:class="`is-registered expire-date ${ getExpireColor(domainRegistration.expireDate) }`">
11+
{{ domainRegistration.expireDate | formatDate }}
12+
</span>
13+
<span v-if="domainMeta.isRegistered"
14+
:class="`is-registered time-left ${getExpireColor(domainRegistration.expireDate) }`">
15+
{{ domainRegistration.expireDate | formatTimeLeft }}
16+
</span>
17+
</div>
18+
<!-- Domain Info -->
19+
<div v-if="showDomainInfo && domainRegistration" class="domain-more-data">
20+
<div class="row">
21+
<span class="lbl">Created</span>
22+
<span class="val">{{ domainRegistration.createdDate | formatDate }}</span>
23+
</div>
24+
<div class="row">
25+
<span class="lbl">Updated</span>
26+
<span class="val">{{ domainRegistration.updatedDate | formatDate }}</span>
27+
</div>
28+
<div class="row">
29+
<span class="lbl">Expires</span>
30+
<span class="val">{{ domainRegistration.expireDate | formatDate }}</span>
31+
</div>
32+
<div class="row" v-for="(ns, inx) in domainRegistration.nameServers" :key="inx">
33+
<span class="lbl">NS {{ inx + 1 }}</span>
34+
<span class="val">{{ ns }}</span>
35+
</div>
36+
<div class="row">
37+
<span class="lbl">Domain ID</span>
38+
<span class="val">{{ domainRegistration.domainId }}</span>
39+
</div>
40+
<div class="row" v-if="domainRegistration.registrar">
41+
<span class="lbl">Registrar</span>
42+
<span class="val">{{ domainRegistration.registrar }}</span>
43+
</div>
44+
<div class="row" v-if="domainRegistration.admin">
45+
<span class="lbl">Admin</span>
46+
<span class="val">{{ domainRegistration.admin }}</span>
47+
</div>
48+
</div>
49+
<!-- Toggle Button -->
50+
<p @click="toggleDomainInfo" class="expend-details-btn" v-if="domainRegistration">
51+
{{ showDomainInfo ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
52+
</p>
53+
</div>
54+
</template>
55+
56+
<script>
57+
import WidgetMixin from '@/mixins/WidgetMixin';
58+
import { timestampToDate, getTimeAgo } from '@/utils/MiscHelpers';
59+
import { widgetApiEndpoints } from '@/utils/defaults';
60+
61+
export default {
62+
mixins: [WidgetMixin],
63+
computed: {
64+
apiKey() {
65+
if (!this.options.apiKey) this.error('Missing API Key');
66+
return this.options.apiKey;
67+
},
68+
domain() {
69+
if (!this.options.domain) this.error('Missing Domain Name Key');
70+
return this.options.domain;
71+
},
72+
endpoint() {
73+
return `${widgetApiEndpoints.domainMonitor}/?domain=${this.domain}&r=whois&apikey=${this.apiKey}`;
74+
},
75+
},
76+
data() {
77+
return {
78+
domainMeta: null,
79+
domainRegistration: null,
80+
showDomainInfo: false,
81+
};
82+
},
83+
filters: {
84+
formatDate(date) {
85+
if (!date) return 'No Date Supplied';
86+
return timestampToDate(date);
87+
},
88+
formatTimeLeft(date) {
89+
return getTimeAgo(new Date(date)).replace('in', '');
90+
},
91+
},
92+
methods: {
93+
/* Make GET request to CoinGecko API endpoint */
94+
fetchData() {
95+
this.makeRequest(this.endpoint).then(this.processData);
96+
},
97+
/* Assign data variables to the returned data */
98+
processData(domainResults) {
99+
if (domainResults.limit_hit) this.error('API Limit Reached');
100+
if (domainResults.status !== '0') this.error(domainResults.status_desc || 'API Error');
101+
// Get domain name and registration status
102+
const domainName = domainResults.domain_name;
103+
const isRegistered = domainResults.registered;
104+
this.domainMeta = { domainName, isRegistered };
105+
// If domain registered, get registration info and expiry dates
106+
if (isRegistered) {
107+
this.domainRegistration = {
108+
expireDate: domainResults.date_expires,
109+
createdDate: domainResults.date_created,
110+
updatedDate: domainResults.date_updated,
111+
nameServers: domainResults.nameservers,
112+
domainId: domainResults.registry_domain_id,
113+
registrar: this.getRegistrar(domainResults.contacts),
114+
admin: this.getAdmin(domainResults.contacts),
115+
};
116+
}
117+
},
118+
getExpireColor(targetDate) {
119+
const now = new Date().getTime();
120+
const then = new Date(targetDate).getTime();
121+
const diff = Math.round((then - now) / (1000 * 60 * 60 * 24));
122+
if (diff < 7) return 'red';
123+
if (diff < 30) return 'orange';
124+
if (diff < 180) return 'yellow';
125+
if (diff >= 180) return 'green';
126+
return 'grey';
127+
},
128+
getRegistrar(contacts) {
129+
if (!Array.isArray(contacts) || contacts.length < 1) return null;
130+
const registrar = contacts.find((contact) => contact.type === 'registrar');
131+
if (registrar) return registrar.name || registrar.organization;
132+
return null;
133+
},
134+
getAdmin(contacts) {
135+
if (!Array.isArray(contacts) || contacts.length < 1) return null;
136+
const accHolder = contacts.find((contact) => contact.type === 'admin')
137+
|| contacts.find((contact) => contact.type === 'registrant');
138+
if (accHolder) return accHolder.name || accHolder.organization;
139+
return null;
140+
},
141+
/* Show / hide full domain info */
142+
toggleDomainInfo() {
143+
this.showDomainInfo = !this.showDomainInfo;
144+
},
145+
},
146+
mounted() {
147+
if (this.options.showFullInfo) this.showDomainInfo = true;
148+
},
149+
};
150+
</script>
151+
152+
<style scoped lang="scss">
153+
.blacklist-check-wrapper {
154+
color: var(--widget-text-color);
155+
padding: 0.25rem;
156+
cursor: default;
157+
overflow: auto;
158+
}
159+
160+
.expiry-wrap {
161+
display: flex;
162+
align-items: center;
163+
justify-content: space-between;
164+
color: var(--widget-text-color);
165+
cursor: default;
166+
font-size: 1.2rem;
167+
font-weight: bold;
168+
span.name {
169+
max-width: 50%;
170+
overflow: hidden;
171+
text-overflow: ellipsis;
172+
}
173+
span.not-registered {
174+
color: var(--info);
175+
}
176+
span.expire-date {
177+
display: none;
178+
white-space: pre;
179+
}
180+
span.expire-date, span.time-left {
181+
&.red { color: var(--danger); }
182+
&.orange { color: var(--error); }
183+
&.yellow { color: var(--warning); }
184+
&.green { color: var(--success); }
185+
&.grey { color: var(--neutral); }
186+
&.blue { color: var(--info); }
187+
}
188+
}
189+
190+
.blacklist-check-wrapper {
191+
&:hover {
192+
.expend-details-btn {
193+
visibility: visible;
194+
}
195+
span.expire-date {
196+
display: block;
197+
}
198+
span.time-left {
199+
display: none;
200+
}
201+
}
202+
}
203+
204+
.expend-details-btn {
205+
visibility: hidden;
206+
margin: 0.2rem;
207+
font-size: 0.8rem;
208+
text-align: center;
209+
opacity: var(--dimming-factor);
210+
cursor: pointer;
211+
}
212+
213+
.domain-more-data {
214+
display: flex;
215+
flex-direction: column;
216+
margin: 0.5rem 0;
217+
.row {
218+
display: flex;
219+
padding: 0.2rem 0;
220+
justify-content: space-between;
221+
opacity: var(--dimming-factor);
222+
color: var(--widget-text-color);
223+
&:not(:last-child) { border-bottom: 1px dashed var(--widget-text-color); }
224+
span.val {
225+
font-family: var(--font-monospace);
226+
max-width: 70%;
227+
overflow: hidden;
228+
text-overflow: ellipsis;
229+
white-space: pre;
230+
&:hover {
231+
max-width: 100%;
232+
}
233+
}
234+
}
235+
}
236+
237+
</style>

‎src/components/Widgets/WidgetBase.vue

+43-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,36 @@
2020
</div>
2121
<!-- Widget -->
2222
<div :class="`widget-wrap ${ error ? 'has-error' : '' }`">
23+
<AdGuardDnsInfo
24+
v-if="widgetType === 'adguard-dns-info'"
25+
:options="widgetOptions"
26+
@loading="setLoaderState"
27+
@error="handleError"
28+
:ref="widgetRef"
29+
/>
30+
<AdGuardFilterStatus
31+
v-else-if="widgetType === 'adguard-filter-status'"
32+
:options="widgetOptions"
33+
@loading="setLoaderState"
34+
@error="handleError"
35+
:ref="widgetRef"
36+
/>
37+
<AdGuardStats
38+
v-else-if="widgetType === 'adguard-stats'"
39+
:options="widgetOptions"
40+
@loading="setLoaderState"
41+
@error="handleError"
42+
:ref="widgetRef"
43+
/>
44+
<AdGuardTopDomains
45+
v-else-if="widgetType === 'adguard-top-domains'"
46+
:options="widgetOptions"
47+
@loading="setLoaderState"
48+
@error="handleError"
49+
:ref="widgetRef"
50+
/>
2351
<AnonAddy
24-
v-if="widgetType === 'anonaddy'"
52+
v-else-if="widgetType === 'anonaddy'"
2553
:options="widgetOptions"
2654
@loading="setLoaderState"
2755
@error="handleError"
@@ -69,6 +97,13 @@
6997
@error="handleError"
7098
:ref="widgetRef"
7199
/>
100+
<DomainMonitor
101+
v-else-if="widgetType === 'domain-monitor'"
102+
:options="widgetOptions"
103+
@loading="setLoaderState"
104+
@error="handleError"
105+
:ref="widgetRef"
106+
/>
72107
<CodeStats
73108
v-else-if="widgetType === 'code-stats'"
74109
:options="widgetOptions"
@@ -421,6 +456,10 @@ export default {
421456
OpenIcon,
422457
LoadingAnimation,
423458
// Register widget components
459+
AdGuardDnsInfo: () => import('@/components/Widgets/AdGuardDnsInfo.vue'),
460+
AdGuardFilterStatus: () => import('@/components/Widgets/AdGuardFilterStatus.vue'),
461+
AdGuardStats: () => import('@/components/Widgets/AdGuardStats.vue'),
462+
AdGuardTopDomains: () => import('@/components/Widgets/AdGuardTopDomains.vue'),
424463
AnonAddy: () => import('@/components/Widgets/AnonAddy.vue'),
425464
Apod: () => import('@/components/Widgets/Apod.vue'),
426465
BlacklistCheck: () => import('@/components/Widgets/BlacklistCheck.vue'),
@@ -430,6 +469,7 @@ export default {
430469
CryptoPriceChart: () => import('@/components/Widgets/CryptoPriceChart.vue'),
431470
CryptoWatchList: () => import('@/components/Widgets/CryptoWatchList.vue'),
432471
CveVulnerabilities: () => import('@/components/Widgets/CveVulnerabilities.vue'),
472+
DomainMonitor: () => import('@/components/Widgets/DomainMonitor.vue'),
433473
EmbedWidget: () => import('@/components/Widgets/EmbedWidget.vue'),
434474
EthGasPrices: () => import('@/components/Widgets/EthGasPrices.vue'),
435475
ExchangeRates: () => import('@/components/Widgets/ExchangeRates.vue'),
@@ -575,7 +615,8 @@ export default {
575615
cursor: not-allowed;
576616
opacity: 0.5;
577617
border-radius: var(--curve-factor);
578-
background: #ffff0080;
618+
background: #ffff0040;
619+
&:hover { background: none; }
579620
}
580621
}
581622
// Error message output

‎src/mixins/ItemMixin.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import axios from 'axios';
33
import router from '@/router';
44
import longPress from '@/directives/LongPress';
5+
import ErrorHandler from '@/utils/ErrorHandler';
56
import {
67
openingMethod as defaultOpeningMethod,
78
serviceEndpoints,
@@ -149,8 +150,7 @@ export default {
149150
router.push({ name: 'workspace', query: { url } });
150151
} else if (this.accumulatedTarget === 'clipboard') {
151152
e.preventDefault();
152-
navigator.clipboard.writeText(url);
153-
this.$toasted.show(this.$t('context-menus.item.copied-toast'));
153+
this.copyToClipboard(url);
154154
}
155155
// Emit event to clear search field, etc
156156
this.$emit('itemClicked');
@@ -178,8 +178,7 @@ export default {
178178
router.push({ name: 'workspace', query: { url } });
179179
break;
180180
case 'clipboard':
181-
navigator.clipboard.writeText(url);
182-
this.$toasted.show(this.$t('context-menus.item.copied-toast'));
181+
this.copyToClipboard(url);
183182
break;
184183
default: window.open(url, '_blank');
185184
}
@@ -199,6 +198,19 @@ export default {
199198
closeContextMenu() {
200199
this.contextMenuOpen = false;
201200
},
201+
/* Copies a string to the users clipboard / shows error if not possible */
202+
copyToClipboard(content) {
203+
if (navigator.clipboard) {
204+
navigator.clipboard.writeText(content);
205+
this.$toasted.show(
206+
this.$t('context-menus.item.copied-toast'),
207+
{ className: 'toast-success' },
208+
);
209+
} else {
210+
ErrorHandler('Clipboard access requires HTTPS. See: https://bit.ly/3N5WuAA');
211+
this.$toasted.show('Unable to copy, see log', { className: 'toast-error' });
212+
}
213+
},
202214
/* Used for smart-sort when sorting items by most used apps */
203215
incrementMostUsedCount(itemId) {
204216
const mostUsed = JSON.parse(localStorage.getItem(localStorageKeys.MOST_USED) || '{}');

‎src/mixins/WidgetMixin.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const WidgetMixin = {
2020
overrideUpdateInterval: null,
2121
disableLoader: false, // Prevent ever showing the loader
2222
updater: null, // Stores interval
23-
defaultTimeout: 10000,
23+
defaultTimeout: 50000,
2424
}),
2525
/* When component mounted, fetch initial data */
2626
mounted() {

‎src/router.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@ import { metaTagData, startingView, routePaths } from '@/utils/defaults';
1919
import ErrorHandler from '@/utils/ErrorHandler';
2020

2121
// Import data from users conf file. Note that rebuild is required for this to update.
22-
import { pages, pageInfo, appConfig } from '../public/conf.yml';
22+
import conf from '../public/conf.yml';
23+
24+
if (!conf) {
25+
ErrorHandler('You\'ve not got any data in your config file yet.');
26+
}
27+
28+
// Assign top-level config fields, check not null
29+
const config = conf || {};
30+
const pages = config.pages || [];
31+
const pageInfo = config.pageInfo || {};
32+
const appConfig = config.appConfig || {};
2333

2434
Vue.use(Router);
2535
const progress = new Progress({ color: 'var(--progress-bar)' });
@@ -50,7 +60,7 @@ const getStartingComponent = () => {
5060

5161
/* Returns the meta tags for each route */
5262
const makeMetaTags = (defaultTitle) => ({
53-
title: pageInfo && pageInfo.title ? pageInfo.title : defaultTitle,
63+
title: pageInfo.title || defaultTitle,
5464
metaTags: metaTagData,
5565
});
5666

‎src/store.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ const store = new Vuex.Store({
5656
return state.config;
5757
},
5858
pageInfo(state) {
59+
if (!state.config) return {};
5960
return state.config.pageInfo || {};
6061
},
6162
appConfig(state) {
63+
if (!state.config) return {};
6264
return state.config.appConfig || {};
6365
},
6466
sections(state) {
@@ -140,8 +142,9 @@ const store = new Vuex.Store({
140142
state.config = config;
141143
},
142144
[SET_REMOTE_CONFIG](state, config) {
143-
if (!config.appConfig) config.appConfig = {};
144-
state.remoteConfig = config;
145+
const notNullConfig = config || {};
146+
if (!notNullConfig.appConfig) notNullConfig.appConfig = {};
147+
state.remoteConfig = notNullConfig;
145148
},
146149
[SET_LANGUAGE](state, lang) {
147150
const newConfig = state.config;

‎src/utils/ConfigAccumalator.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ export default class ConfigAccumulator {
3131
appConfig() {
3232
let appConfigFile = {};
3333
// Set app config from file
34-
if (this.conf) appConfigFile = this.conf.appConfig || buildConf.appConfig || {};
34+
if (this.conf && this.conf.appConfig) {
35+
appConfigFile = this.conf.appConfig;
36+
} else if (buildConf && buildConf.appConfig) {
37+
appConfigFile = buildConf.appConfig;
38+
}
3539
// Fill in defaults if anything missing
3640
let usersAppConfig = defaultAppConfig;
3741
if (localStorage[localStorageKeys.APP_CONFIG]) {

‎src/utils/ErrorHandler.js

+4-7
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,10 @@ const appendToErrorLog = (msg) => {
2222
* If error reporting is enabled, will also log the message to Sentry
2323
* If you wish to use your own error logging service, put code for it here
2424
*/
25-
const ErrorHandler = function handler(msg, errorStack) {
26-
// Print to console
27-
warningMsg(msg, errorStack);
28-
// Save to local storage
29-
appendToErrorLog(msg);
30-
// Report to bug tracker (if enabled)
31-
Sentry.captureMessage(`[USER-WARN] ${msg}`);
25+
export const ErrorHandler = function handler(msg, errorStack) {
26+
warningMsg(msg, errorStack); // Print to console
27+
appendToErrorLog(msg); // Save to local storage
28+
Sentry.captureMessage(`[USER-WARN] ${msg}`); // Report to bug tracker (if enabled)
3229
};
3330

3431
/* Similar to error handler, but for recording general info */

‎src/utils/MiscHelpers.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const sanitize = (string) => {
2828
export const timestampToDate = (timestamp) => {
2929
const localFormat = navigator.language;
3030
const dateFormat = {
31-
weekday: 'short', day: 'numeric', month: 'short', year: '2-digit',
31+
weekday: 'short', day: 'numeric', month: 'short', year: 'numeric',
3232
};
3333
const date = new Date(timestamp).toLocaleDateString(localFormat, dateFormat);
3434
return `${date}`;
@@ -133,16 +133,18 @@ export const getTimeDifference = (startTime, endTime) => {
133133
if (diff < 3600) return `${divide(diff, 60)} minutes`;
134134
if (diff < 86400) return `${divide(diff, 3600)} hours`;
135135
if (diff < 604800) return `${divide(diff, 86400)} days`;
136-
if (diff >= 604800) return `${divide(diff, 604800)} weeks`;
136+
if (diff < 31557600) return `${divide(diff, 604800)} weeks`;
137+
if (diff >= 31557600) return `${divide(diff, 31557600)} years`;
137138
return 'unknown';
138139
};
139140

140141
/* Given a timestamp, return how long ago it was, e.g. '10 minutes' */
141142
export const getTimeAgo = (dateTime) => {
142143
const now = new Date().getTime();
144+
const isHistorical = new Date(dateTime).getTime() < now;
143145
const diffStr = getTimeDifference(dateTime, now);
144146
if (diffStr === 'unknown') return diffStr;
145-
return `${diffStr} ago`;
147+
return isHistorical ? `${diffStr} ago` : `in ${diffStr}`;
146148
};
147149

148150
/* Given the name of a CSS variable, returns it's value */

‎src/utils/defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ module.exports = {
224224
cryptoPrices: 'https://api.coingecko.com/api/v3/coins/',
225225
cryptoWatchList: 'https://api.coingecko.com/api/v3/coins/markets/',
226226
cveVulnerabilities: 'https://www.cvedetails.com/json-feed.php',
227+
domainMonitor: 'https://api.whoapi.com',
227228
ethGasPrices: 'https://ethgas.watch/api/gas',
228229
ethGasHistory: 'https://ethgas.watch/api/gas/trend',
229230
exchangeRates: 'https://v6.exchangerate-api.com/v6/',

‎yarn.lock

+10,617-11,061
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.