🔀 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:
Alicia Sykes 2022-06-04 21:41:43 +01:00 committed by GitHub
commit e451f86e3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 11803 additions and 11167 deletions

View File

@ -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)

View File

@ -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

View File

@ -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/`

View File

@ -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)

View File

@ -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.

View File

@ -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/).

View File

@ -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)",

View File

@ -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() {

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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

View File

@ -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) || '{}');

View File

@ -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() {

View File

@ -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,
});

View File

@ -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;

View File

@ -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]) {

View File

@ -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 */

View File

@ -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 */

View File

@ -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/',

21678
yarn.lock

File diff suppressed because it is too large Load Diff