mirror of
https://github.com/Lissy93/dashy.git
synced 2024-09-11 22:27:10 +03:00
🔀 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
This commit is contained in:
commit
e451f86e3e
3
.github/CHANGELOG.md
vendored
3
.github/CHANGELOG.md
vendored
@ -1,5 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## ✨ 2.0.9 Adds Multi-Page Support [PR #685](https://github.com/Lissy93/dashy/pull/685)
|
||||
- Adds Widgets for AdGuard
|
||||
|
||||
## ✨ 2.0.9 Adds Multi-Page Support [PR #663](https://github.com/Lissy93/dashy/pull/663)
|
||||
- Fix KeyCloak API URL (#564)
|
||||
- Fix guest has config access (#590)
|
||||
|
19
.github/LATEST_CHANGELOG.md
vendored
19
.github/LATEST_CHANGELOG.md
vendored
@ -1,17 +1,2 @@
|
||||
## ✨ 2.0.9 Adds Multi-Page Support [PR #663](https://github.com/Lissy93/dashy/pull/663)
|
||||
- Fix KeyCloak API URL (#564)
|
||||
- Fix guest has config access (#590)
|
||||
- Fix collapsible content in multi-page support (#626)
|
||||
- Fix layout and item size buttons ( #629)
|
||||
- Refactor make request in RSS widget (#632)
|
||||
- Fix material-design-icons header in schema (#640)
|
||||
- Add option to hide seconds in clock widget (#644)
|
||||
- Fix pageInfo not being read in router (#645)
|
||||
- Fix startingView not honored (#646)
|
||||
- Fix Status Check default (#651)
|
||||
- Add option to hide image in SportsScores Widget (#654)
|
||||
- Add Adventure-basic theme (#655)
|
||||
- Write docs for sub-items (#657)
|
||||
- Add Font-Awesome displaying as square to troubleshooting guide (#659)
|
||||
- Show expand / collapse in context menu (#660)
|
||||
- Only deploy new release when relevant files have changed
|
||||
## ✨ 2.0.9 Adds Multi-Page Support [PR #685](https://github.com/Lissy93/dashy/pull/685)
|
||||
- Adds Widgets for AdGuard
|
||||
|
@ -111,6 +111,8 @@ Dashy supports [Widgets](/docs/widgets.md) for displaying dynamic content. Below
|
||||
- [IP-API Privacy Policy](https://ip-api.com/docs/legal)
|
||||
- **[IP Blacklist](/docs/widgets.md#ip-blacklist)**: `https://api.blacklistchecker.com`
|
||||
- [Blacklist Checker Privacy Policy](https://blacklistchecker.com/privacy)
|
||||
- **[Domain Monitor](/docs/widgets.md#domain-monitor)**: `http://api.whoapi.com`
|
||||
- [WhoAPI Privacy Policy](https://whoapi.com/privacy-policy/)
|
||||
- **[Crypto Watch List](/docs/widgets.md#crypto-watch-list)** and **[Token Price History](/docs/widgets.md#crypto-token-price-history)**: `https://api.coingecko.com`
|
||||
- [CoinGecko Privacy Policy](https://www.coingecko.com/en/privacy)
|
||||
- **[Wallet Balance](/docs/widgets.md#wallet-balance)**: `https://api.blockcypher.com/`
|
||||
|
124
docs/showcase.md
124
docs/showcase.md
@ -10,19 +10,74 @@
|
||||
---
|
||||
|
||||
### Ratty222
|
||||
> By [@ratty222](https://github.com/ratty222) <sup>[#384](https://github.com/Lissy93/dashy/discussions/384)</sup>
|
||||
> By [@ratty222](https://github.com/ratty222) <sup>Re: [#384](https://github.com/Lissy93/dashy/discussions/384)</sup>
|
||||
|
||||
![screenshot-ratty222-dashy](https://user-images.githubusercontent.com/1862727/147582551-4c655d37-8bcc-4f95-ab41-164a9d0d6a07.png)
|
||||
|
||||
---
|
||||
|
||||
### Hugalafutro Dashy
|
||||
> By [@hugalafutro](https://github.com/hugalafutro) <sup>[#505](https://github.com/Lissy93/dashy/discussions/505)</sup>
|
||||
> By [@hugalafutro](https://github.com/hugalafutro) <sup>Re: [#505](https://github.com/Lissy93/dashy/discussions/505)</sup>
|
||||
|
||||
[![hugalafutro-dashy-screenshot](https://i.ibb.co/PDpLDKS/hugalafutro-dashy.gif)](https://i.ibb.co/PDpLDKS/hugalafutro-dashy.gif)
|
||||
|
||||
---
|
||||
|
||||
### NAS Home Dashboard
|
||||
> By [@cerealconyogurt](https://github.com/cerealconyogurt) <sup>Re: [#74](https://github.com/Lissy93/dashy/issues/74)</sup>
|
||||
|
||||
![screenshot-networking-services](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/6-nas-home-dashboard.png)
|
||||
|
||||
---
|
||||
|
||||
### Brewhack
|
||||
|
||||
> By [@brpeterso](https://github.com/brpeterso) <sup>Re: [#680](https://github.com/Lissy93/dashy/issues/680)</sup>
|
||||
|
||||
![screenshot-brewhack-dashboard](https://i.ibb.co/cNjzPT4/brewhack.png)
|
||||
|
||||
---
|
||||
|
||||
### The Private Dashboard
|
||||
|
||||
> By [@DylanBeMe](https://github.com/DylanBeMe) <sup>Re: [#419](https://github.com/Lissy93/dashy/issues/419)</sup>
|
||||
|
||||
![screenshot-private-dashboard](https://i.ibb.co/hKS483T/private-dashboard-Dylan-Be-Me.png)
|
||||
|
||||
---
|
||||
|
||||
### Homelab & VPS dashboard
|
||||
> By [@shadowking001](https://github.com/shadowking001) <sup>Re: [#86](https://github.com/Lissy93/dashy/issues/86)</sup>
|
||||
|
||||
![screenshot-shadowking001-dashy](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/8-shadowking001s-dashy.png)
|
||||
|
||||
---
|
||||
|
||||
### Raspberry PI Docker Dashboard
|
||||
|
||||
> By [@henkiewie](https://github.com/henkiewie) <sup>Re: [#622](https://github.com/Lissy93/dashy/issues/622)</sup>
|
||||
|
||||
> 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`
|
||||
|
||||
![screenshot-henkiewie-dashy](https://i.ibb.co/jGzPm6b/henkiewie-dashy-showcase.png)
|
||||
|
||||
---
|
||||
|
||||
### First Week of Self-Hosting
|
||||
> 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>
|
||||
|
||||
![screenshot-week-of-self-hosting](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/11-ricky-cz.png)
|
||||
|
||||
---
|
||||
|
||||
### EVO Dashboard
|
||||
|
||||
> By [@EVOTk](https://github.com/EVOTk) <sup>Re: [#316](https://github.com/Lissy93/dashy/pull/316)</sup>
|
||||
|
||||
![screenshot-evo-dashboard](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/12-evo-dashboard.png)
|
||||
|
||||
---
|
||||
|
||||
### Networking Services
|
||||
> By [@Lissy93](https://github.com/lissy93)
|
||||
|
||||
@ -30,36 +85,6 @@
|
||||
|
||||
---
|
||||
|
||||
### Homelab & VPS dashboard
|
||||
> By [@shadowking001](https://github.com/shadowking001)
|
||||
|
||||
![screenshot-shadowking001-dashy](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/8-shadowking001s-dashy.png)
|
||||
|
||||
---
|
||||
|
||||
### EVO Dashboard
|
||||
|
||||
> By [@EVOTk](https://github.com/EVOTk)
|
||||
|
||||
![screenshot-evo-dashboard](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/12-evo-dashboard.png)
|
||||
|
||||
---
|
||||
|
||||
### The Private Dashboard
|
||||
|
||||
> By [@DylanBeMe](https://github.com/DylanBeMe) <sup>[#419](https://github.com/Lissy93/dashy/issues/419)</sup>
|
||||
|
||||
![screenshot-evo-dashboard](https://i.ibb.co/hKS483T/private-dashboard-Dylan-Be-Me.png)
|
||||
|
||||
---
|
||||
|
||||
### NAS Home Dashboard
|
||||
> By [@cerealconyogurt](https://github.com/cerealconyogurt)
|
||||
|
||||
![screenshot-networking-services](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/6-nas-home-dashboard.png)
|
||||
|
||||
---
|
||||
|
||||
### Dashy Live
|
||||
> By [@Lissy93](https://github.com/lissy93)
|
||||
|
||||
@ -108,16 +133,9 @@
|
||||
|
||||
---
|
||||
|
||||
### First Week of Self-Hosting
|
||||
> By [u//RickyCZ](https://www.reddit.com/user/RickyCZ)
|
||||
|
||||
![screenshot-week-of-self-hosting](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/11-ricky-cz.png)
|
||||
|
||||
---
|
||||
|
||||
### HomeLAb 3.0
|
||||
|
||||
> By [@skoogee](https://github.com/skoogee) (http://zhrn.cc)
|
||||
> By [@skoogee](https://github.com/skoogee) (http://zhrn.cc) <sup>[#279](https://github.com/Lissy93/dashy/issues/279)</sup>
|
||||
|
||||
> 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.
|
||||
|
||||
@ -126,19 +144,12 @@
|
||||
---
|
||||
|
||||
### Ground Control
|
||||
> By [@dtctek](https://github.com/dtctek)
|
||||
> By [@dtctek](https://github.com/dtctek) <sup>Re: [#83](https://github.com/Lissy93/dashy/issues/83)</sup>
|
||||
|
||||
![screenshot-ground-control](https://user-images.githubusercontent.com/1862727/149821995-e9b41dab-186c-42e6-b5b3-e233259b241d.png)
|
||||
|
||||
---
|
||||
|
||||
### Morning Dashboard
|
||||
> Displayed on my smart screen between 05:00 - 08:00, and includes all the info that I usually check before leaving for work
|
||||
|
||||
![screenshot-morning-dash](https://i.ibb.co/4Wx8zb7/morning-dashboard.png)
|
||||
|
||||
---
|
||||
|
||||
### Croco_Grievous
|
||||
> 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>
|
||||
|
||||
@ -154,16 +165,6 @@
|
||||
|
||||
---
|
||||
|
||||
### Raspberry PI Docker Dashboard
|
||||
|
||||
> By [@henkiewie](https://github.com/henkiewie) <sup>via [#622](https://github.com/Lissy93/dashy/issues/622)</sup>
|
||||
|
||||
> 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`
|
||||
|
||||
![screenshot-henkiewie-dashy](https://i.ibb.co/jGzPm6b/henkiewie-dashy-showcase.png)
|
||||
|
||||
---
|
||||
|
||||
### Stefantigro
|
||||
> 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>
|
||||
|
||||
@ -171,6 +172,13 @@
|
||||
|
||||
---
|
||||
|
||||
### Morning Dashboard
|
||||
> Displayed on my smart screen between 05:00 - 08:00, and includes all the info that I usually check before leaving for work
|
||||
|
||||
![screenshot-morning-dash](https://i.ibb.co/4Wx8zb7/morning-dashboard.png)
|
||||
|
||||
---
|
||||
|
||||
### Yet Another Homelab
|
||||
|
||||
![screenshot-yet-another-homelab](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/9-home-lab-oblivion.png)
|
||||
|
@ -31,6 +31,7 @@
|
||||
- [Fixing Widget CORS Errors](#widget-cors-errors)
|
||||
- [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 Open Browser Console](#how-to-open-browser-console)
|
||||
- [Git Contributions not Displaying](#git-contributions-not-displaying)
|
||||
|
||||
@ -436,6 +437,17 @@ Finally, check the [browser console](#how-to-open-browser-console) for any error
|
||||
|
||||
---
|
||||
|
||||
## Copy to Clipboard not Working
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
As a workaround, you could either:
|
||||
- Highlight the text and copy / <kbd>Ctrl</kbd> + <kbd>C</kbd>
|
||||
- Or setup SSL - [here's a guide](https://github.com/Lissy93/dashy/blob/master/docs/management.md#ssl-certificates) on doing so
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
|
194
docs/widgets.md
194
docs/widgets.md
@ -2,10 +2,6 @@
|
||||
|
||||
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.
|
||||
|
||||
> ℹ️ **Note**: Widgets are still in the Alpha-phase of development.
|
||||
> If you find a bug, please raise it.<br>
|
||||
> Adding / editing widgets through the UI isn't yet supported, you will need to do this in the YAML config file.
|
||||
|
||||
##### Contents
|
||||
- **[General Widgets](#general-widgets)**
|
||||
- [Clock](#clock)
|
||||
@ -15,6 +11,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
|
||||
- [Image](#image)
|
||||
- [Public IP Address](#public-ip)
|
||||
- [IP Blacklist Checker](#ip-blacklist)
|
||||
- [Domain Monitor](#domain-monitor)
|
||||
- [Crypto Watch List](#crypto-watch-list)
|
||||
- [Crypto Price History](#crypto-token-price-history)
|
||||
- [Crypto Wallet Balance](#wallet-balance)
|
||||
@ -47,6 +44,10 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
|
||||
- [Recent Traffic](#recent-traffic)
|
||||
- [Stat Ping Statuses](#stat-ping-statuses)
|
||||
- [Synology Download Station](#synology-download-station)
|
||||
- [AdGuard Home Block Stats](#adguard-home-block-stats)
|
||||
- [AdGuard Home Filters](#adguard-home-filters)
|
||||
- [AdGuard Home DNS Info](#adguard-home-dns-info)
|
||||
- [AdGuard Home Top Domains](#adguard-home-top-domains)
|
||||
- **[System Resource Monitoring](#system-resource-monitoring)**
|
||||
- [CPU Usage Current](#current-cpu-usage)
|
||||
- [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
|
||||
|
||||
---
|
||||
|
||||
### Domain Monitor
|
||||
|
||||
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.
|
||||
|
||||
<p align="center"><img width="600" src="https://i.ibb.co/7XjByG9/domain-monitor.png" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`domain`** | `string` | Required | The domain to check
|
||||
**`apiKey`** | `string` | Required | You can get your free API key from [my.whoapi.com](https://my.whoapi.com/user/signup)
|
||||
**`showFullInfo`** | `boolean` | _Optional_ | If set to true, the toggle-full-info panel will be open by default
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: domain-monitor
|
||||
options:
|
||||
domain: example.com
|
||||
apiKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
- type: domain-monitor
|
||||
options:
|
||||
domain: example2.com
|
||||
apiKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟢 Enabled
|
||||
- **Auth**: 🔴 Required
|
||||
- **Price**: 🟠 Free Plan (10,000 requests)
|
||||
- **Host**: Managed Instance Only
|
||||
- **Privacy**: _See [WhoAPI Privacy Policy](https://whoapi.com/privacy-policy/)_
|
||||
|
||||
---
|
||||
|
||||
### Crypto Watch List
|
||||
|
||||
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
|
||||
|
||||
Displays the current downloads/torrents tasks of your Synology NAS
|
||||
|
||||
<p align="center"><img width="300" src="https://i.ibb.co/N2kKWTN/image.png" /></p>
|
||||
<p align="center"><img width="500" src="https://i.ibb.co/N2kKWTN/image.png" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
@ -1365,6 +1403,152 @@ Displays the current downloads/torrents tasks of your Synology NAS
|
||||
|
||||
---
|
||||
|
||||
### AdGuard Home Block Stats
|
||||
|
||||
Fetches data from your [AdGuard Home](https://adguard.com/en/adguard-home/overview.html) instance, and
|
||||
displays total number of allowed and blocked queries, plus a pie chart showing breakdown by block type.
|
||||
|
||||
<p align="center"><img width="400" src="https://i.ibb.co/qgkcxsN/adguard-block-percent-2.png" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL to your AdGuard Home instance
|
||||
**`username`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your username here
|
||||
**`password`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your password here
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: adguard-stats
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: http://127.0.0.1
|
||||
username: admin
|
||||
password: test
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟠 Optional
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [AdGuard Home](https://adguard.com/en/adguard-home/overview.html))
|
||||
- **Privacy**: _See [AdGuard Privacy Policy](https://adguard.com/en/privacy.html)_
|
||||
|
||||
|
||||
---
|
||||
|
||||
### AdGuard Home Filters
|
||||
|
||||
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.
|
||||
|
||||
<p align="center"><img width="400" src="https://i.ibb.co/WsJkf5g/adguard-filters-list.png" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL to your AdGuard Home instance
|
||||
**`username`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your username here
|
||||
**`password`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your password here
|
||||
**`showOnOffStatusOnly`** | `boolean` | _Optional_ | If set to `true`, will only show aggregated AdGuard filter status (on/off), instead of a list of filters
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: adguard-filter-status
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: http://127.0.0.1
|
||||
username: admin
|
||||
password: test
|
||||
showOnOffStatusOnly: false
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟠 Optional
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [AdGuard Home](https://adguard.com/en/adguard-home/overview.html))
|
||||
- **Privacy**: _See [AdGuard Privacy Policy](https://adguard.com/en/privacy.html)_
|
||||
|
||||
---
|
||||
|
||||
### AdGuard Home DNS Info
|
||||
|
||||
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.
|
||||
|
||||
<p align="center"><img width="400" src="https://i.ibb.co/G0JngBb/adguard-dns-info.png" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL to your AdGuard Home instance
|
||||
**`username`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your username here
|
||||
**`password`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your password here
|
||||
**`showFullInfo`** | `boolean` | _Optional_ | If set to `true`, the full DNS info will be shown by default, without having to click "Show Info"
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: adguard-dns-info
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: http://127.0.0.1
|
||||
username: admin
|
||||
password: test
|
||||
showFullInfo: false
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟠 Optional
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [AdGuard Home](https://adguard.com/en/adguard-home/overview.html))
|
||||
- **Privacy**: _See [AdGuard Privacy Policy](https://adguard.com/en/privacy.html)_
|
||||
|
||||
---
|
||||
|
||||
### AdGuard Home Top Domains
|
||||
|
||||
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.
|
||||
|
||||
<p align="center"><img width="600" src="https://i.ibb.co/qRhYYTk/adguard-top-domains.png" /></p>
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`hostname`** | `string` | Required | The URL to your AdGuard Home instance
|
||||
**`username`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your username here
|
||||
**`password`** | `string` | _Optional_ | If you've got auth enabled on AdGuard, provide your password here
|
||||
**`limit`** | `number` | _Optional_ | Specify the number of results to show, between `1` and `100`, defaults to `10`
|
||||
**`hideBlockedDomains`** | `boolean` | _Optional_ | Don't show the blocked domains list (queried domains only)
|
||||
**`hideQueriedDomains`** | `boolean` | _Optional_ | Don't show the queried domains list (blocked domains only)
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- type: adguard-top-domains
|
||||
useProxy: true
|
||||
options:
|
||||
hostname: http://127.0.0.1
|
||||
username: admin
|
||||
password: test
|
||||
limit: 10
|
||||
```
|
||||
|
||||
##### Info
|
||||
- **CORS**: 🟠 Proxied
|
||||
- **Auth**: 🟠 Optional
|
||||
- **Price**: 🟢 Free
|
||||
- **Host**: Self-Hosted (see [AdGuard Home](https://adguard.com/en/adguard-home/overview.html))
|
||||
- **Privacy**: _See [AdGuard Privacy Policy](https://adguard.com/en/privacy.html)_
|
||||
|
||||
---
|
||||
|
||||
## System Resource Monitoring
|
||||
|
||||
The easiest method for displaying system info and resource usage in Dashy is with [Glances](https://nicolargo.github.io/glances/).
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Dashy",
|
||||
"version": "2.0.9",
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
"main": "server",
|
||||
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
||||
|
@ -38,7 +38,7 @@ import { modalNames } from '@/utils/defaults';
|
||||
import AccessError from '@/components/Configuration/AccessError';
|
||||
import DownloadConfigIcon from '@/assets/interface-icons/config-download-file.svg';
|
||||
import CopyConfigIcon from '@/assets/interface-icons/interactive-editor-copy-clipboard.svg';
|
||||
import { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||
import { ErrorHandler, InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||
|
||||
export default {
|
||||
name: 'ExportConfigMenu',
|
||||
@ -80,8 +80,13 @@ export default {
|
||||
},
|
||||
copyConfigToClipboard() {
|
||||
const config = this.convertJsonToYaml();
|
||||
navigator.clipboard.writeText(config);
|
||||
this.$toasted.show(this.$t('config.data-copied-msg'));
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(config);
|
||||
this.$toasted.show(this.$t('config.data-copied-msg'));
|
||||
} else {
|
||||
ErrorHandler('Clipboard access requires HTTPS. See: https://bit.ly/3N5WuAA');
|
||||
this.$toasted.show('Unable to copy, see log', { className: 'toast-error' });
|
||||
}
|
||||
InfoHandler('Config copied to clipboard', InfoKeys.EDITOR);
|
||||
},
|
||||
modalClosed() {
|
||||
|
174
src/components/Widgets/AdGuardDnsInfo.vue
Normal file
174
src/components/Widgets/AdGuardDnsInfo.vue
Normal file
@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="ad-guard-dns-info-wrapper">
|
||||
<div class="enable-status" v-if="enabled !== null">
|
||||
<p v-if="enabled" class="status connected"><span>✔</span> Enabled</p>
|
||||
<p v-else class="status not-connected"><span>✘</span> Disabled</p>
|
||||
</div>
|
||||
<p @click="toggleShowData" v-if="dnsInfo.length > 0" class="expend-details-btn">
|
||||
{{ showData ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
|
||||
</p>
|
||||
<div v-if="showData && dnsInfo.length > 0" class="dns-info">
|
||||
<div v-for="(item, index) in dnsInfo" :key="index" class="row">
|
||||
<span class="lbl">{{ item.lbl }}: </span>
|
||||
<span class="val">{{ item.val | renderVal }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { capitalize } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
computed: {
|
||||
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
|
||||
return this.options.hostname;
|
||||
},
|
||||
showFullInfo() {
|
||||
return this.options.showFullInfo;
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/control/dns_info`;
|
||||
},
|
||||
basicInoEndpoint() {
|
||||
return `${this.hostname}/control/status`;
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
enabled: null,
|
||||
dnsInfo: [],
|
||||
showData: false,
|
||||
};
|
||||
},
|
||||
filters: {
|
||||
renderVal(val) {
|
||||
if (val === undefined) return 'N/A';
|
||||
if (Array.isArray(val) && val.length === 0) return 'N/A';
|
||||
if (typeof val === 'boolean') return val ? '✔' : '✘';
|
||||
if (typeof val === 'string') return capitalize(val);
|
||||
if (Array.isArray(val)) return val.join('\n');
|
||||
return val;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Make GET request to AdGuard endpoint */
|
||||
fetchData() {
|
||||
this.makeRequest(this.basicInoEndpoint, this.authHeaders).then(this.processStatusBasics);
|
||||
this.makeRequest(this.endpoint, this.authHeaders).then(this.processData);
|
||||
},
|
||||
processStatusBasics(data) {
|
||||
const newInfo = [
|
||||
{ lbl: 'DNS Address', val: data.dns_addresses },
|
||||
{ lbl: 'DNS Port', val: data.dns_port },
|
||||
{ lbl: 'HTTP Port', val: data.http_port },
|
||||
];
|
||||
this.dnsInfo = [...this.dnsInfo, ...newInfo];
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
this.enabled = data.protection_enabled;
|
||||
const newInfo = [
|
||||
{ lbl: 'Blocking Mode', val: data.blocking_mode },
|
||||
{ lbl: 'Cache Size', val: `${data.cache_size} B` },
|
||||
{ lbl: 'IPv6', val: !data.disable_ipv6 },
|
||||
{ lbl: 'DNSSEC', val: data.dnssec_enabled },
|
||||
{ lbl: 'EDNS Client-Subnet', val: data.edns_cs_enabled },
|
||||
{ lbl: 'Private PTR', val: data.use_private_ptr_resolvers },
|
||||
{ lbl: 'Upstream DNS', val: data.upstream_dns },
|
||||
{ lbl: 'PRT Upstream', val: data.local_ptr_upstreams },
|
||||
{ lbl: 'Bootstrap DNS', val: data.bootstrap_dns },
|
||||
];
|
||||
this.dnsInfo = [...this.dnsInfo, ...newInfo];
|
||||
},
|
||||
toggleShowData() {
|
||||
this.showData = !this.showData;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.showFullInfo) this.showData = true;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ad-guard-dns-info-wrapper {
|
||||
color: var(--widget-text-color);
|
||||
.enable-status {
|
||||
.status {
|
||||
display: flex;
|
||||
max-width: 250px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
margin: 0.25rem auto;
|
||||
justify-content: space-evenly;
|
||||
span {
|
||||
font-size: 1.5rem;
|
||||
border-radius: 1.5rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border: 1px solid;
|
||||
color: var(--background);
|
||||
}
|
||||
&.not-connected {
|
||||
color: var(--danger);
|
||||
span { background: var(--danger); }
|
||||
}
|
||||
&.connected {
|
||||
color: var(--success);
|
||||
span { background: var(--success); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.expend-details-btn {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border: 1px solid transparent;
|
||||
color: var(--widget-text-color);
|
||||
opacity: var(--dimming-factor);
|
||||
border-radius: var(--curve-factor);
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:focus, &:active {
|
||||
background: var(--widget-text-color);
|
||||
color: var(--widget-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dns-info {
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.1rem;
|
||||
font-size: 0.9rem;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
.val {
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
143
src/components/Widgets/AdGuardFilterStatus.vue
Normal file
143
src/components/Widgets/AdGuardFilterStatus.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="ad-guard-filter-status-wrapper">
|
||||
<!-- Current Status -->
|
||||
<div v-if="status !== null && showOnOffStatusOnly" class="status">
|
||||
<span class="status-lbl">{{ $t('widgets.pi-hole.status-heading') }}:</span>
|
||||
<span :class="`status-val ${getStatusColor(status)}`">
|
||||
{{ status ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- List of filters -->
|
||||
<div v-if="filters && !showOnOffStatusOnly" class="filters-list">
|
||||
<div v-for="filter in filters" :key="filter.id" class="filter">
|
||||
<!-- Filter status, name and query count -->
|
||||
<div class="row-1">
|
||||
<span :class="`on-off ${filter.enabled ? 'green' : 'red'}`">
|
||||
{{ filter.enabled ? '✔' : '✘' }}
|
||||
</span>
|
||||
<span class="filter-name">{{ filter.name }}</span>
|
||||
<span class="filter-rules-count">{{ filter.rules_count }}</span>
|
||||
</div>
|
||||
<!-- Date of last update, and link to list -->
|
||||
<div class="row-2">
|
||||
<span class="updated">Updated {{ filter.last_updated | formatDate }}</span>
|
||||
<a class="filter-link" v-if="filter.url" :href="filter.url">View List</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { getTimeAgo } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
computed: {
|
||||
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
|
||||
return this.options.hostname;
|
||||
},
|
||||
showOnOffStatusOnly() {
|
||||
return this.options.showOnOffStatusOnly;
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/control/filtering/status`;
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: null,
|
||||
filters: null,
|
||||
};
|
||||
},
|
||||
filters: {
|
||||
formatDate(date) {
|
||||
if (!date) return 'Never';
|
||||
return getTimeAgo(date);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Make GET request to AdGuard endpoint */
|
||||
fetchData() {
|
||||
this.makeRequest(this.endpoint, this.authHeaders).then(this.processData);
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
this.status = data.enabled;
|
||||
this.filters = data.filters;
|
||||
},
|
||||
getStatusColor(status) {
|
||||
return status ? 'green' : 'red';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ad-guard-filter-status-wrapper {
|
||||
.status {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
.status-lbl {
|
||||
color: var(--widget-text-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
.status-val {
|
||||
font-family: var(--font-monospace);
|
||||
&.green { color: var(--success); }
|
||||
&.red { color: var(--danger); }
|
||||
&.blue { color: var(--info); }
|
||||
}
|
||||
}
|
||||
.filters-list {
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--widget-text-color);
|
||||
padding: 0.25rem 0.1rem;
|
||||
.row-1 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
span.on-off {
|
||||
margin-right: 0.5rem;
|
||||
&.green { color: var(--success); }
|
||||
&.red { color: var(--danger); }
|
||||
}
|
||||
span.filter-name {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
span.rules_count {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
}
|
||||
.row-2 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
span.updated, a.filter-link {
|
||||
margin: 0.2rem 0;
|
||||
font-size: 0.8rem;
|
||||
opacity: var(--dimming-factor);
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
130
src/components/Widgets/AdGuardStats.vue
Normal file
130
src/components/Widgets/AdGuardStats.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div class="ad-guard-stats-wrapper">
|
||||
<!-- Show total query and block count -->
|
||||
<div v-if="queryCount && blockCount" class="summary">
|
||||
<div><span class="lbl">Queries:</span><span class="val">{{ queryCount }}</span></div>
|
||||
<div><span class="lbl">Blocked:</span><span class="val">{{ blockCount }}</span></div>
|
||||
</div>
|
||||
<!-- Pie chart with block breakdown -->
|
||||
<p :id="chartId" class="block-pie"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin, ChartingMixin],
|
||||
computed: {
|
||||
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
|
||||
return this.options.hostname;
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/control/stats`;
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
queryCount: null,
|
||||
blockCount: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* Make GET request to AdGuard endpoint */
|
||||
fetchData() {
|
||||
this.makeRequest(this.endpoint, this.authHeaders).then(this.processData);
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
// Get data from response, to be rendered to the chart
|
||||
const totalAllowed = data.num_dns_queries || 0;
|
||||
const blocked = data.num_blocked_filtering || 0;
|
||||
const safeBrowsing = data.num_replaced_safebrowsing || 0;
|
||||
const safeSearch = data.num_replaced_safesearch || 0;
|
||||
const parental = data.num_replaced_parental || 0;
|
||||
const blockTotal = blocked + safeBrowsing + safeSearch + parental;
|
||||
const remaining = totalAllowed - blockTotal;
|
||||
|
||||
// Set query and block count, for first line
|
||||
this.queryCount = totalAllowed;
|
||||
this.blockCount = blockTotal;
|
||||
|
||||
// Put data into a flat array for the chart
|
||||
const chartColors = ['#ef476f', '#06d6a0'];
|
||||
const chartValues = [blocked, remaining];
|
||||
const chartLabels = ['Blocked', 'Allowed'];
|
||||
|
||||
// If additional blocked results are non-zero, append to chart data
|
||||
if (safeBrowsing > 0) {
|
||||
chartColors.push('#ffc43d');
|
||||
chartValues.push(safeBrowsing);
|
||||
chartLabels.push('Safe Search - Blocked');
|
||||
}
|
||||
if (safeSearch > 0) {
|
||||
chartColors.push('#f8ffe5');
|
||||
chartValues.push(safeSearch);
|
||||
chartLabels.push('Safe Search - Blocked');
|
||||
}
|
||||
if (parental > 0) {
|
||||
chartColors.push('#1b9aaa');
|
||||
chartValues.push(parental);
|
||||
chartLabels.push('Parental Controls - Blocked');
|
||||
}
|
||||
|
||||
// Call generate chart function
|
||||
this.generateBlockPie(chartLabels, chartValues, chartColors);
|
||||
},
|
||||
/* Generate pie chart showing the proportion of queries blocked */
|
||||
generateBlockPie(labels, values, colors) {
|
||||
return new this.Chart(`#${this.chartId}`, {
|
||||
title: 'AdGuard DNS Queries',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{ values }],
|
||||
},
|
||||
type: 'donut',
|
||||
height: 250,
|
||||
strokeWidth: 20,
|
||||
colors,
|
||||
tooltipOptions: {
|
||||
formatTooltipY: d => `${Math.round(d)} queries`,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ad-guard-stats-wrapper {
|
||||
.block-pie {
|
||||
margin: 0;
|
||||
svg.frappe-chart.chart {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
color: var(--widget-text-color);
|
||||
span.lbl {
|
||||
font-weight: bold;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
span.val {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
127
src/components/Widgets/AdGuardTopDomains.vue
Normal file
127
src/components/Widgets/AdGuardTopDomains.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="ad-guard-top-domains-wrapper">
|
||||
<!-- List of top blocked domains -->
|
||||
<div class="sec blocked-domains" v-if="topBlockedDomains && !hideBlockedDomains">
|
||||
<h3 class="sub-title">Top Blocked Domains</h3>
|
||||
<div class="row title-row">
|
||||
<span class="cell domain">Domain</span>
|
||||
<span class="cell">Query Count</span>
|
||||
</div>
|
||||
<div class="row" v-for="(domain, ind) in topBlockedDomains" :key="ind">
|
||||
<span class="cell domain">{{ domain.name }}</span>
|
||||
<span class="cell count">{{ domain.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- List of top queried domains -->
|
||||
<div class="sec blocked-domains" v-if="topQueriedDomains && !hideQueriedDomains">
|
||||
<h3 class="sub-title">Top Queried Domains</h3>
|
||||
<div class="row title-row">
|
||||
<span class="cell domain">Domain</span>
|
||||
<span class="cell">Query Count</span>
|
||||
</div>
|
||||
<div class="row" v-for="(domain, ind) in topQueriedDomains" :key="ind">
|
||||
<span class="cell domain">{{ domain.name }}</span>
|
||||
<span class="cell count">{{ domain.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
computed: {
|
||||
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
|
||||
hostname() {
|
||||
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
|
||||
return this.options.hostname;
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
|
||||
return { Authorization: `Basic ${encoded}` };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
limit() {
|
||||
return this.options.limit || 10;
|
||||
},
|
||||
hideBlockedDomains() {
|
||||
return this.options.hideBlockedDomains;
|
||||
},
|
||||
hideQueriedDomains() {
|
||||
return this.options.hideQueriedDomains;
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/control/stats`;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
topQueriedDomains: null,
|
||||
topBlockedDomains: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* Make GET request to AdGuard endpoint */
|
||||
fetchData() {
|
||||
this.makeRequest(this.endpoint, this.authHeaders).then(this.processData);
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
this.topQueriedDomains = this.makeDomainData(data.top_queried_domains);
|
||||
this.topBlockedDomains = this.makeDomainData(data.top_blocked_domains);
|
||||
},
|
||||
/* Process AdGruard's weird data format, into something that can be rendered */
|
||||
makeDomainData(rawData) {
|
||||
const domains = [];
|
||||
rawData.forEach((domainBlock) => {
|
||||
Object.keys(domainBlock).forEach((domain) => {
|
||||
domains.push({ name: domain, count: domainBlock[domain] });
|
||||
});
|
||||
});
|
||||
return domains.slice(0, this.limit);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ad-guard-top-domains-wrapper {
|
||||
text-align: center;
|
||||
color: var(--widget-text-color);
|
||||
.sec {
|
||||
width: 100%;
|
||||
max-width: 28rem;
|
||||
margin-right: 1rem;
|
||||
display: inline-block;
|
||||
h3.sub-title {
|
||||
text-align: left;
|
||||
font-size: 1.2rem;
|
||||
margin: 0.4rem 0 0.2rem 0;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
font-size: 0.9rem;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.1rem;
|
||||
justify-content: space-between;
|
||||
color: var(--widget-text-color);
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
&.title-row {
|
||||
font-weight: bold;
|
||||
border-top: 1px solid var(--widget-text-color);
|
||||
}
|
||||
.cell {
|
||||
&.count {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
237
src/components/Widgets/DomainMonitor.vue
Normal file
237
src/components/Widgets/DomainMonitor.vue
Normal file
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="blacklist-check-wrapper">
|
||||
<!-- Domain Name and Registration State / Expiry Count Down -->
|
||||
<div class="expiry-wrap" v-if="domainMeta" @click="toggleDomainInfo">
|
||||
<span class="name">{{ domainMeta.domainName }}</span>
|
||||
<span v-if="!domainMeta.isRegistered" class="not-registered">
|
||||
Not Registered
|
||||
</span>
|
||||
<span v-if="domainMeta.isRegistered"
|
||||
:class="`is-registered expire-date ${ getExpireColor(domainRegistration.expireDate) }`">
|
||||
{{ domainRegistration.expireDate | formatDate }}
|
||||
</span>
|
||||
<span v-if="domainMeta.isRegistered"
|
||||
:class="`is-registered time-left ${getExpireColor(domainRegistration.expireDate) }`">
|
||||
{{ domainRegistration.expireDate | formatTimeLeft }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Domain Info -->
|
||||
<div v-if="showDomainInfo && domainRegistration" class="domain-more-data">
|
||||
<div class="row">
|
||||
<span class="lbl">Created</span>
|
||||
<span class="val">{{ domainRegistration.createdDate | formatDate }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="lbl">Updated</span>
|
||||
<span class="val">{{ domainRegistration.updatedDate | formatDate }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="lbl">Expires</span>
|
||||
<span class="val">{{ domainRegistration.expireDate | formatDate }}</span>
|
||||
</div>
|
||||
<div class="row" v-for="(ns, inx) in domainRegistration.nameServers" :key="inx">
|
||||
<span class="lbl">NS {{ inx + 1 }}</span>
|
||||
<span class="val">{{ ns }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="lbl">Domain ID</span>
|
||||
<span class="val">{{ domainRegistration.domainId }}</span>
|
||||
</div>
|
||||
<div class="row" v-if="domainRegistration.registrar">
|
||||
<span class="lbl">Registrar</span>
|
||||
<span class="val">{{ domainRegistration.registrar }}</span>
|
||||
</div>
|
||||
<div class="row" v-if="domainRegistration.admin">
|
||||
<span class="lbl">Admin</span>
|
||||
<span class="val">{{ domainRegistration.admin }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Toggle Button -->
|
||||
<p @click="toggleDomainInfo" class="expend-details-btn" v-if="domainRegistration">
|
||||
{{ showDomainInfo ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { timestampToDate, getTimeAgo } from '@/utils/MiscHelpers';
|
||||
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
computed: {
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('Missing API Key');
|
||||
return this.options.apiKey;
|
||||
},
|
||||
domain() {
|
||||
if (!this.options.domain) this.error('Missing Domain Name Key');
|
||||
return this.options.domain;
|
||||
},
|
||||
endpoint() {
|
||||
return `${widgetApiEndpoints.domainMonitor}/?domain=${this.domain}&r=whois&apikey=${this.apiKey}`;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainMeta: null,
|
||||
domainRegistration: null,
|
||||
showDomainInfo: false,
|
||||
};
|
||||
},
|
||||
filters: {
|
||||
formatDate(date) {
|
||||
if (!date) return 'No Date Supplied';
|
||||
return timestampToDate(date);
|
||||
},
|
||||
formatTimeLeft(date) {
|
||||
return getTimeAgo(new Date(date)).replace('in', '');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Make GET request to CoinGecko API endpoint */
|
||||
fetchData() {
|
||||
this.makeRequest(this.endpoint).then(this.processData);
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(domainResults) {
|
||||
if (domainResults.limit_hit) this.error('API Limit Reached');
|
||||
if (domainResults.status !== '0') this.error(domainResults.status_desc || 'API Error');
|
||||
// Get domain name and registration status
|
||||
const domainName = domainResults.domain_name;
|
||||
const isRegistered = domainResults.registered;
|
||||
this.domainMeta = { domainName, isRegistered };
|
||||
// If domain registered, get registration info and expiry dates
|
||||
if (isRegistered) {
|
||||
this.domainRegistration = {
|
||||
expireDate: domainResults.date_expires,
|
||||
createdDate: domainResults.date_created,
|
||||
updatedDate: domainResults.date_updated,
|
||||
nameServers: domainResults.nameservers,
|
||||
domainId: domainResults.registry_domain_id,
|
||||
registrar: this.getRegistrar(domainResults.contacts),
|
||||
admin: this.getAdmin(domainResults.contacts),
|
||||
};
|
||||
}
|
||||
},
|
||||
getExpireColor(targetDate) {
|
||||
const now = new Date().getTime();
|
||||
const then = new Date(targetDate).getTime();
|
||||
const diff = Math.round((then - now) / (1000 * 60 * 60 * 24));
|
||||
if (diff < 7) return 'red';
|
||||
if (diff < 30) return 'orange';
|
||||
if (diff < 180) return 'yellow';
|
||||
if (diff >= 180) return 'green';
|
||||
return 'grey';
|
||||
},
|
||||
getRegistrar(contacts) {
|
||||
if (!Array.isArray(contacts) || contacts.length < 1) return null;
|
||||
const registrar = contacts.find((contact) => contact.type === 'registrar');
|
||||
if (registrar) return registrar.name || registrar.organization;
|
||||
return null;
|
||||
},
|
||||
getAdmin(contacts) {
|
||||
if (!Array.isArray(contacts) || contacts.length < 1) return null;
|
||||
const accHolder = contacts.find((contact) => contact.type === 'admin')
|
||||
|| contacts.find((contact) => contact.type === 'registrant');
|
||||
if (accHolder) return accHolder.name || accHolder.organization;
|
||||
return null;
|
||||
},
|
||||
/* Show / hide full domain info */
|
||||
toggleDomainInfo() {
|
||||
this.showDomainInfo = !this.showDomainInfo;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.options.showFullInfo) this.showDomainInfo = true;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.blacklist-check-wrapper {
|
||||
color: var(--widget-text-color);
|
||||
padding: 0.25rem;
|
||||
cursor: default;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.expiry-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--widget-text-color);
|
||||
cursor: default;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
span.name {
|
||||
max-width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
span.not-registered {
|
||||
color: var(--info);
|
||||
}
|
||||
span.expire-date {
|
||||
display: none;
|
||||
white-space: pre;
|
||||
}
|
||||
span.expire-date, span.time-left {
|
||||
&.red { color: var(--danger); }
|
||||
&.orange { color: var(--error); }
|
||||
&.yellow { color: var(--warning); }
|
||||
&.green { color: var(--success); }
|
||||
&.grey { color: var(--neutral); }
|
||||
&.blue { color: var(--info); }
|
||||
}
|
||||
}
|
||||
|
||||
.blacklist-check-wrapper {
|
||||
&:hover {
|
||||
.expend-details-btn {
|
||||
visibility: visible;
|
||||
}
|
||||
span.expire-date {
|
||||
display: block;
|
||||
}
|
||||
span.time-left {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expend-details-btn {
|
||||
visibility: hidden;
|
||||
margin: 0.2rem;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
opacity: var(--dimming-factor);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.domain-more-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.5rem 0;
|
||||
.row {
|
||||
display: flex;
|
||||
padding: 0.2rem 0;
|
||||
justify-content: space-between;
|
||||
opacity: var(--dimming-factor);
|
||||
color: var(--widget-text-color);
|
||||
&:not(:last-child) { border-bottom: 1px dashed var(--widget-text-color); }
|
||||
span.val {
|
||||
font-family: var(--font-monospace);
|
||||
max-width: 70%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre;
|
||||
&:hover {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -20,8 +20,36 @@
|
||||
</div>
|
||||
<!-- Widget -->
|
||||
<div :class="`widget-wrap ${ error ? 'has-error' : '' }`">
|
||||
<AdGuardDnsInfo
|
||||
v-if="widgetType === 'adguard-dns-info'"
|
||||
: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-if="widgetType === 'anonaddy'"
|
||||
v-else-if="widgetType === 'anonaddy'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
@ -69,6 +97,13 @@
|
||||
@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"
|
||||
@ -421,6 +456,10 @@ export default {
|
||||
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'),
|
||||
@ -430,6 +469,7 @@ export default {
|
||||
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'),
|
||||
@ -575,7 +615,8 @@ export default {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
border-radius: var(--curve-factor);
|
||||
background: #ffff0080;
|
||||
background: #ffff0040;
|
||||
&:hover { background: none; }
|
||||
}
|
||||
}
|
||||
// Error message output
|
||||
|
@ -2,6 +2,7 @@
|
||||
import axios from 'axios';
|
||||
import router from '@/router';
|
||||
import longPress from '@/directives/LongPress';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import {
|
||||
openingMethod as defaultOpeningMethod,
|
||||
serviceEndpoints,
|
||||
@ -149,8 +150,7 @@ export default {
|
||||
router.push({ name: 'workspace', query: { url } });
|
||||
} else if (this.accumulatedTarget === 'clipboard') {
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(url);
|
||||
this.$toasted.show(this.$t('context-menus.item.copied-toast'));
|
||||
this.copyToClipboard(url);
|
||||
}
|
||||
// Emit event to clear search field, etc
|
||||
this.$emit('itemClicked');
|
||||
@ -178,8 +178,7 @@ export default {
|
||||
router.push({ name: 'workspace', query: { url } });
|
||||
break;
|
||||
case 'clipboard':
|
||||
navigator.clipboard.writeText(url);
|
||||
this.$toasted.show(this.$t('context-menus.item.copied-toast'));
|
||||
this.copyToClipboard(url);
|
||||
break;
|
||||
default: window.open(url, '_blank');
|
||||
}
|
||||
@ -199,6 +198,19 @@ export default {
|
||||
closeContextMenu() {
|
||||
this.contextMenuOpen = false;
|
||||
},
|
||||
/* Copies a string to the users clipboard / shows error if not possible */
|
||||
copyToClipboard(content) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(content);
|
||||
this.$toasted.show(
|
||||
this.$t('context-menus.item.copied-toast'),
|
||||
{ className: 'toast-success' },
|
||||
);
|
||||
} else {
|
||||
ErrorHandler('Clipboard access requires HTTPS. See: https://bit.ly/3N5WuAA');
|
||||
this.$toasted.show('Unable to copy, see log', { className: 'toast-error' });
|
||||
}
|
||||
},
|
||||
/* Used for smart-sort when sorting items by most used apps */
|
||||
incrementMostUsedCount(itemId) {
|
||||
const mostUsed = JSON.parse(localStorage.getItem(localStorageKeys.MOST_USED) || '{}');
|
||||
|
@ -20,7 +20,7 @@ const WidgetMixin = {
|
||||
overrideUpdateInterval: null,
|
||||
disableLoader: false, // Prevent ever showing the loader
|
||||
updater: null, // Stores interval
|
||||
defaultTimeout: 10000,
|
||||
defaultTimeout: 50000,
|
||||
}),
|
||||
/* When component mounted, fetch initial data */
|
||||
mounted() {
|
||||
|
@ -19,7 +19,17 @@ import { metaTagData, startingView, routePaths } from '@/utils/defaults';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
|
||||
// Import data from users conf file. Note that rebuild is required for this to update.
|
||||
import { pages, pageInfo, appConfig } from '../public/conf.yml';
|
||||
import conf from '../public/conf.yml';
|
||||
|
||||
if (!conf) {
|
||||
ErrorHandler('You\'ve not got any data in your config file yet.');
|
||||
}
|
||||
|
||||
// Assign top-level config fields, check not null
|
||||
const config = conf || {};
|
||||
const pages = config.pages || [];
|
||||
const pageInfo = config.pageInfo || {};
|
||||
const appConfig = config.appConfig || {};
|
||||
|
||||
Vue.use(Router);
|
||||
const progress = new Progress({ color: 'var(--progress-bar)' });
|
||||
@ -50,7 +60,7 @@ const getStartingComponent = () => {
|
||||
|
||||
/* Returns the meta tags for each route */
|
||||
const makeMetaTags = (defaultTitle) => ({
|
||||
title: pageInfo && pageInfo.title ? pageInfo.title : defaultTitle,
|
||||
title: pageInfo.title || defaultTitle,
|
||||
metaTags: metaTagData,
|
||||
});
|
||||
|
||||
|
@ -56,9 +56,11 @@ const store = new Vuex.Store({
|
||||
return state.config;
|
||||
},
|
||||
pageInfo(state) {
|
||||
if (!state.config) return {};
|
||||
return state.config.pageInfo || {};
|
||||
},
|
||||
appConfig(state) {
|
||||
if (!state.config) return {};
|
||||
return state.config.appConfig || {};
|
||||
},
|
||||
sections(state) {
|
||||
@ -140,8 +142,9 @@ const store = new Vuex.Store({
|
||||
state.config = config;
|
||||
},
|
||||
[SET_REMOTE_CONFIG](state, config) {
|
||||
if (!config.appConfig) config.appConfig = {};
|
||||
state.remoteConfig = config;
|
||||
const notNullConfig = config || {};
|
||||
if (!notNullConfig.appConfig) notNullConfig.appConfig = {};
|
||||
state.remoteConfig = notNullConfig;
|
||||
},
|
||||
[SET_LANGUAGE](state, lang) {
|
||||
const newConfig = state.config;
|
||||
|
@ -31,7 +31,11 @@ export default class ConfigAccumulator {
|
||||
appConfig() {
|
||||
let appConfigFile = {};
|
||||
// Set app config from file
|
||||
if (this.conf) appConfigFile = this.conf.appConfig || buildConf.appConfig || {};
|
||||
if (this.conf && this.conf.appConfig) {
|
||||
appConfigFile = this.conf.appConfig;
|
||||
} else if (buildConf && buildConf.appConfig) {
|
||||
appConfigFile = buildConf.appConfig;
|
||||
}
|
||||
// Fill in defaults if anything missing
|
||||
let usersAppConfig = defaultAppConfig;
|
||||
if (localStorage[localStorageKeys.APP_CONFIG]) {
|
||||
|
@ -22,13 +22,10 @@ const appendToErrorLog = (msg) => {
|
||||
* If error reporting is enabled, will also log the message to Sentry
|
||||
* If you wish to use your own error logging service, put code for it here
|
||||
*/
|
||||
const ErrorHandler = function handler(msg, errorStack) {
|
||||
// Print to console
|
||||
warningMsg(msg, errorStack);
|
||||
// Save to local storage
|
||||
appendToErrorLog(msg);
|
||||
// Report to bug tracker (if enabled)
|
||||
Sentry.captureMessage(`[USER-WARN] ${msg}`);
|
||||
export const ErrorHandler = function handler(msg, errorStack) {
|
||||
warningMsg(msg, errorStack); // Print to console
|
||||
appendToErrorLog(msg); // Save to local storage
|
||||
Sentry.captureMessage(`[USER-WARN] ${msg}`); // Report to bug tracker (if enabled)
|
||||
};
|
||||
|
||||
/* Similar to error handler, but for recording general info */
|
||||
|
@ -28,7 +28,7 @@ export const sanitize = (string) => {
|
||||
export const timestampToDate = (timestamp) => {
|
||||
const localFormat = navigator.language;
|
||||
const dateFormat = {
|
||||
weekday: 'short', day: 'numeric', month: 'short', year: '2-digit',
|
||||
weekday: 'short', day: 'numeric', month: 'short', year: 'numeric',
|
||||
};
|
||||
const date = new Date(timestamp).toLocaleDateString(localFormat, dateFormat);
|
||||
return `${date}`;
|
||||
@ -133,16 +133,18 @@ export const getTimeDifference = (startTime, endTime) => {
|
||||
if (diff < 3600) return `${divide(diff, 60)} minutes`;
|
||||
if (diff < 86400) return `${divide(diff, 3600)} hours`;
|
||||
if (diff < 604800) return `${divide(diff, 86400)} days`;
|
||||
if (diff >= 604800) return `${divide(diff, 604800)} weeks`;
|
||||
if (diff < 31557600) return `${divide(diff, 604800)} weeks`;
|
||||
if (diff >= 31557600) return `${divide(diff, 31557600)} years`;
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
/* Given a timestamp, return how long ago it was, e.g. '10 minutes' */
|
||||
export const getTimeAgo = (dateTime) => {
|
||||
const now = new Date().getTime();
|
||||
const isHistorical = new Date(dateTime).getTime() < now;
|
||||
const diffStr = getTimeDifference(dateTime, now);
|
||||
if (diffStr === 'unknown') return diffStr;
|
||||
return `${diffStr} ago`;
|
||||
return isHistorical ? `${diffStr} ago` : `in ${diffStr}`;
|
||||
};
|
||||
|
||||
/* Given the name of a CSS variable, returns it's value */
|
||||
|
@ -224,6 +224,7 @@ module.exports = {
|
||||
cryptoPrices: 'https://api.coingecko.com/api/v3/coins/',
|
||||
cryptoWatchList: 'https://api.coingecko.com/api/v3/coins/markets/',
|
||||
cveVulnerabilities: 'https://www.cvedetails.com/json-feed.php',
|
||||
domainMonitor: 'https://api.whoapi.com',
|
||||
ethGasPrices: 'https://ethgas.watch/api/gas',
|
||||
ethGasHistory: 'https://ethgas.watch/api/gas/trend',
|
||||
exchangeRates: 'https://v6.exchangerate-api.com/v6/',
|
||||
|
Loading…
Reference in New Issue
Block a user