🔀 Merge pull request #557 from Lissy93/FEATURE/user-suggestions

[FEATURE] Implementing user suggestions
This PR resolves loads of tickets... Fixes #492, fixes #494, fixes #497, fixes #505, fixes #522, fixes #524, fixes #546, fixes #552, fixes #554, fixes #560, fixes #564, fixes #568, fixes #570, fixes #575 and fixes #576 - Yay 🥳
Celebrating getting out of hospital with 15 bug fixes 🐛 and a beer 🍺
This commit is contained in:
Alicia Sykes 2022-03-28 10:51:32 +01:00 committed by GitHub
commit 93a531a3f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 236 additions and 63 deletions

25
.github/CHANGELOG.md vendored
View File

@ -1,18 +1,35 @@
# Changelog # Changelog
## 2.0.5 - Bug Fixes and a few New Features ## 🐛 2.0.6 Fixes user requested issues [PR #557](https://github.com/Lissy93/dashy/pull/557)
- Allows middle click open new tab, Re: #492
- Implements Max redirects for status checks, Re: #494
- Adds Gitpod config for cloud-ready IDE, Re: #497
- Adss new screenshots to showcase, Re: #505
- Fixes excess space below footer, Re: #522
- Allows iframe content to be viewed full-screen, Re: #524
- Fixes Glances widgets with Authorization headers, Re: #546
- Adds target attribute to nav links, Re: #552
- Removes fixed max-width on wide-screens, Re: #554
- Adds missing type attribute to external CSS, Re: #560
- Updates path to Keycloak API, Re: #564
- Fixes link to @walkxhub homelab icons, Re #568
- Fixes local image path on sub-page, Re: #570
- Adds typecheck on edit item tags, Re: #575
- Fixes item size in config not honored, Re: #576
### Partially revert 2.0.4, fixing several issues caused by `conf.yml` not being loaded at startup. ## ✨ 2.0.5 - Bug Fixes and a few New Features
#### Partially revert 2.0.4, fixing several issues caused by `conf.yml` not being loaded at startup.
This change requires a rebuild of the application when several options under `appConfig` are changed. This change requires a rebuild of the application when several options under `appConfig` are changed.
Fixes #544 #555 Fixes #544 #555
### Several other changes since 2.0.4, including: #### Several other changes since 2.0.4, including:
The `Add New Section` button on the UI editor now displays if no sections are present. #536 The `Add New Section` button on the UI editor now displays if no sections are present. #536
When using SSL, the server can now redirect from HTTP to HTTPS. This is enabled by default when using SSL. #538 When using SSL, the server can now redirect from HTTP to HTTPS. This is enabled by default when using SSL. #538
Section context menus are now accessible on mobile, and will no longer clip off the screen. #541 Section context menus are now accessible on mobile, and will no longer clip off the screen. #541
Italian translations have been added. #556 Italian translations have been added. #556
## :sparkles: 2.0.4 - Dynamic Config Loading [PR #528](https://github.com/Lissy93/dashy/pull/528) ## 2.0.4 - Dynamic Config Loading [PR #528](https://github.com/Lissy93/dashy/pull/528)
- `conf.yml` is now loaded dynamically and the app now only needs a browser refresh on config change, not a full rebuild! - `conf.yml` is now loaded dynamically and the app now only needs a browser refresh on config change, not a full rebuild!
## 🐛 2.0.3 - Bug Fixes [PR #488](https://github.com/Lissy93/dashy/pull/488) ## 🐛 2.0.3 - Bug Fixes [PR #488](https://github.com/Lissy93/dashy/pull/488)

View File

@ -1,9 +1,16 @@
### Partially revert 2.0.4, fixing several issues caused by `conf.yml` not being loaded at startup. ## 🐛 Fixes user requested issues [PR #557](https://github.com/Lissy93/dashy/pull/557)
This change requires a rebuild of the application when several options under `appConfig` are changed. - Allows middle click open new tab, Re: #492
Fixes #544 #555 - Implements Max redirects for status checks, Re: #494
- Adds Gitpod config for cloud-ready IDE, Re: #497
### Several other changes since 2.0.4, including: - Adss new screenshots to showcase, Re: #505
The `Add New Section` button on the UI editor now displays if no sections are present. #536 - Fixes excess space below footer, Re: #522
When using SSL, the server can now redirect from HTTP to HTTPS. This is enabled by default when using SSL. #538 - Allows iframe content to be viewed full-screen, Re: #524
Section context menus are now accessible on mobile, and will no longer clip off the screen. #541 - Fixes Glances widgets with Authorization headers, Re: #546
Italian translations have been added. #556 - Adds target attribute to nav links, Re: #552
- Removes fixed max-width on wide-screens, Re: #554
- Adds missing type attribute to external CSS, Re: #560
- Updates path to Keycloak API, Re: #564
- Fixes link to @walkxhub homelab icons, Re #568
- Fixes local image path on sub-page, Re: #570
- Adds typecheck on edit item tags, Re: #575
- Fixes item size in config not honored, Re: #576

29
.gitpod.yml Normal file
View File

@ -0,0 +1,29 @@
# Config for running Dashy in GitPod's cloud dev environment
# Docs: https://www.gitpod.io/docs/references/gitpod-yml
# Commands to start on workspace startup
tasks:
- init: yarn install
command: yarn dev
# Ports to expose on workspace startup
ports:
- port: 8080 # Default dev server
visibility: private
onOpen: open-preview
- port: 4000 # Default prod server
visibility: public
onOpen: open-preview
prebuilds:
# Adds 'Open in GitPod' to PRs
addBadge: true
addComment: false
vscode:
# Adds Vue.js and formatting extensions
extensions:
- octref.vetur
- dbaeumer.vscode-eslint
- streetsidesoftware.code-spell-checker
- PKief.material-icon-theme
- wix.vscode-import-cost
- oderwat.indent-rainbow
- eamodio.gitlens

View File

@ -77,6 +77,7 @@ The following file provides a reference of all supported configuration options.
--- | --- | --- | --- --- | --- | --- | ---
**`title`** | `string` | Required | The text to display on the link button **`title`** | `string` | Required | The text to display on the link button
**`path`** | `string` | Required | The URL to navigate to when clicked. Can be relative (e.g. `/about`) or absolute (e.g. `https://example.com` or `http://192.168.1.1`) **`path`** | `string` | Required | The URL to navigate to when clicked. Can be relative (e.g. `/about`) or absolute (e.g. `https://example.com` or `http://192.168.1.1`)
**`target`** | `string` | _Optional_ | The opening method (external links only). Can be either `newtab`, `sametab`, `top` or `parent`. Defaults to `newtab`
**[⬆️ Back to Top](#configuring)** **[⬆️ Back to Top](#configuring)**
@ -206,6 +207,7 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**`statusCheckHeaders`** | `object` | _Optional_ | If you're endpoint requires any specific headers for the status checking, then define them here **`statusCheckHeaders`** | `object` | _Optional_ | If you're endpoint requires any specific headers for the status checking, then define them here
**`statusCheckAllowInsecure`** | `boolean` | _Optional_ | By default, any request to insecure content will be blocked. Setting this option to `true` will disable the `rejectUnauthorized` option, enabling you to ping non-HTTPS services for the current item. Defaults to `false` **`statusCheckAllowInsecure`** | `boolean` | _Optional_ | By default, any request to insecure content will be blocked. Setting this option to `true` will disable the `rejectUnauthorized` option, enabling you to ping non-HTTPS services for the current item. Defaults to `false`
**`statusCheckAcceptCodes`** | `string` | _Optional_ | If your service's response code is anything other than 2xx, then you can opt to specify an alternative success code. E.g. if you expect your server to return 403, but still want the status indicator to be green, set this value to `403` **`statusCheckAcceptCodes`** | `string` | _Optional_ | If your service's response code is anything other than 2xx, then you can opt to specify an alternative success code. E.g. if you expect your server to return 403, but still want the status indicator to be green, set this value to `403`
**`statusCheckMaxRedirects`** | `number` | _Optional_ | If your service redirects to another page, and you would like status checks to follow redirects, then specify the maximum number of redirects here. Defaults to `0` / will not follow redirects
**`color`** | `string` | _Optional_ | An optional color for the text and font-awesome icon to be displayed in. Note that this will override the current theme and so may not display well **`color`** | `string` | _Optional_ | An optional color for the text and font-awesome icon to be displayed in. Note that this will override the current theme and so may not display well
**`backgroundColor`** | `string` | _Optional_ | An optional background fill color for the that given item. Again, this will override the current theme and so might not display well against the background **`backgroundColor`** | `string` | _Optional_ | An optional background fill color for the that given item. Again, this will override the current theme and so might not display well against the background
**`provider`** | `string` | _Optional_ | The name of the provider for a given service, useful for when including hosted apps. In some themes, this is visible under the item name **`provider`** | `string` | _Optional_ | The name of the provider for a given service, useful for when including hosted apps. In some themes, this is visible under the item name
@ -220,6 +222,7 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**`options`** | `object` | _Optional_ | Some widgets accept either optional or required additional options. Again, see the [Widget Docs](/docs/widgets.md) for full list of options **`options`** | `object` | _Optional_ | Some widgets accept either optional or required additional options. Again, see the [Widget Docs](/docs/widgets.md) for full list of options
**`updateInterval`** | `number` | _Optional_ | You can keep a widget constantly updated by specifying an update interval, in seconds. See [Continuous Updates Docs](/docs/widgets.md#continuous-updates) for more info **`updateInterval`** | `number` | _Optional_ | You can keep a widget constantly updated by specifying an update interval, in seconds. See [Continuous Updates Docs](/docs/widgets.md#continuous-updates) for more info
**`useProxy`** | `boolean` | _Optional_ | Some widgets make API requests to services that are not CORS-enabled. For these instances, you will need to route requests through a proxy, Dashy has a built in CORS-proxy, which you can use by setting this option to `true`. Defaults to `false`. See the [Proxying Requests Docs](/docs/widgets.md#proxying-requests) for more info **`useProxy`** | `boolean` | _Optional_ | Some widgets make API requests to services that are not CORS-enabled. For these instances, you will need to route requests through a proxy, Dashy has a built in CORS-proxy, which you can use by setting this option to `true`. Defaults to `false`. See the [Proxying Requests Docs](/docs/widgets.md#proxying-requests) for more info
**`timeout`** | `number` | _Optional_ | Request timeout in milliseconds, defaults to ½ a second (`500`)
**[⬆️ Back to Top](#configuring)** **[⬆️ Back to Top](#configuring)**

View File

@ -16,6 +16,13 @@
--- ---
### Hugalafutro Dashy
> By [@hugalafutro](https://github.com/hugalafutro) <sup>[#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)
---
### Networking Services ### Networking Services
> By [@Lissy93](https://github.com/lissy93) > By [@Lissy93](https://github.com/lissy93)
@ -126,6 +133,13 @@
--- ---
### 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>
![screenshot-croco-grievous-dashy](https://i.ibb.co/59XR8KL/dashy-Croco-Grievous.png)
---
### Crypto Dash ### Crypto Dash
> Example usage of widgets to monitor cryptocurrencies news, prices and data. Config is [available here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10#file-example-8-dashy-crypto-widgets-conf-yml) > Example usage of widgets to monitor cryptocurrencies news, prices and data. Config is [available here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10#file-example-8-dashy-crypto-widgets-conf-yml)
@ -134,6 +148,13 @@
--- ---
### 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>
![screenshot-stefantigro-dashy](https://i.ibb.co/1Kb43Yy/dashy-stefantigro.png)
---
### Yet Another Homelab ### Yet Another Homelab
![screenshot-yet-another-homelab](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/9-home-lab-oblivion.png) ![screenshot-yet-another-homelab](https://raw.githubusercontent.com/Lissy93/dashy/master/docs/showcase/9-home-lab-oblivion.png)

View File

@ -1289,6 +1289,7 @@ All Glance's based widgets require a `hostname`. All other parameters are option
**`apiVersion`** | `string` | _Optional_ | Specify an API version, defaults to V `3`. Note that support for older versions is limited **`apiVersion`** | `string` | _Optional_ | Specify an API version, defaults to V `3`. Note that support for older versions is limited
**`limit`** | `number` | _Optional_ | For widgets that show a time-series chart, optionally limit the number of data points returned. A higher number will show more historical results, but will take longer to load. A value between 300 - 800 is usually optimal **`limit`** | `number` | _Optional_ | For widgets that show a time-series chart, optionally limit the number of data points returned. A higher number will show more historical results, but will take longer to load. A value between 300 - 800 is usually optimal
Note that if auth is configured, requests must be proxied with `useProxy: true`
##### Info ##### Info
- **CORS**: 🟢 Enabled - **CORS**: 🟢 Enabled
- **Auth**: 🟠 Optional - **Auth**: 🟠 Optional
@ -1726,6 +1727,12 @@ Vary: Origin
--- ---
### Setting Timeout
Default timeout is ½ a second. This can be overridden with the `timeout` attribute on a widget, specified as an integer in milliseconds.
---
### Widget Styling ### Widget Styling
Like elsewhere in Dashy, all colours can be easily modified with CSS variables. Like elsewhere in Dashy, all colours can be easily modified with CSS variables.

View File

@ -1,6 +1,6 @@
{ {
"name": "Dashy", "name": "Dashy",
"version": "2.0.5", "version": "2.0.6",
"license": "MIT", "license": "MIT",
"main": "server", "main": "server",
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)", "author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",

View File

@ -28,15 +28,23 @@ const makeErrorMessage2 = (data) => '❌ Service Error - '
+ `${data.status} - ${data.statusText}`; + `${data.status} - ${data.statusText}`;
/* Kicks of a HTTP request, then formats and renders results */ /* Kicks of a HTTP request, then formats and renders results */
const makeRequest = (url, headers, insecure, acceptCodes, render) => { const makeRequest = (url, options, render) => {
console.log(options);
const {
headers, enableInsecure, acceptCodes, maxRedirects,
} = options;
const validCodes = acceptCodes && acceptCodes !== 'null' ? acceptCodes : null; const validCodes = acceptCodes && acceptCodes !== 'null' ? acceptCodes : null;
const startTime = new Date(); const startTime = new Date();
const requestMaker = axios.create({ const requestMaker = axios.create({
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
rejectUnauthorized: !insecure, rejectUnauthorized: !enableInsecure,
}), }),
}); });
requestMaker.get(url, { headers }) requestMaker.request({
url,
headers,
maxRedirects,
})
.then((response) => { .then((response) => {
const statusCode = response.status; const statusCode = response.status;
const { statusText } = response; const { statusText } = response;
@ -100,9 +108,13 @@ module.exports = (paramStr, render) => {
const params = new URLSearchParams(paramStr); const params = new URLSearchParams(paramStr);
const url = decodeURIComponent(params.get('url')); const url = decodeURIComponent(params.get('url'));
const acceptCodes = decodeURIComponent(params.get('acceptCodes')); const acceptCodes = decodeURIComponent(params.get('acceptCodes'));
const maxRedirects = decodeURIComponent(params.get('maxRedirects')) || 0;
const headers = decodeHeaders(params.get('headers')); const headers = decodeHeaders(params.get('headers'));
const enableInsecure = !!params.get('enableInsecure'); const enableInsecure = !!params.get('enableInsecure');
if (!url || url === 'undefined') immediateError(render); if (!url || url === 'undefined') immediateError(render);
makeRequest(url, headers, enableInsecure, acceptCodes, render); const options = {
headers, enableInsecure, acceptCodes, maxRedirects,
};
makeRequest(url, options, render);
} }
}; };

View File

@ -1,5 +1,5 @@
<template> <template>
<div id="dashy"> <div id="dashy" :style="topLevelStyleModifications">
<EditModeTopBanner v-if="isEditMode" /> <EditModeTopBanner v-if="isEditMode" />
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" /> <LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
<Header :pageInfo="pageInfo" /> <Header :pageInfo="pageInfo" />
@ -72,6 +72,17 @@ export default {
isEditMode() { isEditMode() {
return this.$store.state.editMode; return this.$store.state.editMode;
}, },
topLevelStyleModifications() {
const vc = this.visibleComponents;
if (!vc.footer && !vc.pageTitle) {
return '--footer-height: 1rem;';
} else if (!vc.footer) {
return '--footer-height: 5rem;';
} else if (!vc.pageTitle) {
return '--footer-height: 4rem;';
}
return '';
},
}, },
methods: { methods: {
/* Injects the users custom CSS as a style tag */ /* Injects the users custom CSS as a style tag */

View File

@ -231,8 +231,8 @@ export default {
const newItem = item; const newItem = item;
newItem.id = this.itemId; newItem.id = this.itemId;
if (newItem.hotkey) newItem.hotkey = parseInt(newItem.hotkey, 10); if (newItem.hotkey) newItem.hotkey = parseInt(newItem.hotkey, 10);
const strToTags = (str) => { const strToTags = (tags) => {
const tagArr = str.split(','); const tagArr = (typeof tags === 'string') ? tags.split(',') : tags;
return tagArr.map((tag) => tag.trim().toLowerCase().replace(/[^a-z0-9]+/, '')); return tagArr.map((tag) => tag.trim().toLowerCase().replace(/[^a-z0-9]+/, ''));
}; };
const strToBool = (str) => { const strToBool = (str) => {

View File

@ -3,7 +3,7 @@
classes="dashy-modal"> classes="dashy-modal">
<div slot="top-right" @click="hide()">Close</div> <div slot="top-right" @click="hide()">Close</div>
<a @click="hide()" class="close-button" title="Close">x</a> <a @click="hide()" class="close-button" title="Close">x</a>
<iframe v-if="url" :src="url" @keydown.esc="close" class="frame"/> <iframe v-if="url" :src="url" @keydown.esc="close" class="frame" allow="fullscreen" />
<div v-else class="no-url">No URL Specified</div> <div v-else class="no-url">No URL Specified</div>
</modal> </modal>
</template> </template>

View File

@ -3,7 +3,7 @@
<a @click="itemOpened" <a @click="itemOpened"
@mouseup.right="openContextMenu" @mouseup.right="openContextMenu"
@contextmenu.prevent @contextmenu.prevent
:href="hyperLinkHref" :href="url"
:target="anchorTarget" :target="anchorTarget"
:class="`item ${makeClassList}`" :class="`item ${makeClassList}`"
v-tooltip="getTooltipOptions()" v-tooltip="getTooltipOptions()"
@ -96,6 +96,7 @@ export default {
statusCheckInterval: Number, // Num seconds beteween repeating checks statusCheckInterval: Number, // Num seconds beteween repeating checks
statusCheckAllowInsecure: Boolean, // Status check ignore SSL certs statusCheckAllowInsecure: Boolean, // Status check ignore SSL certs
statusCheckAcceptCodes: String, // Allow status checks to pass with a code other than 200 statusCheckAcceptCodes: String, // Allow status checks to pass with a code other than 200
statusCheckMaxRedirects: Number, // Specify max number of redirects
parentSectionTitle: String, // Title of parent section (for add new) parentSectionTitle: String, // Title of parent section (for add new)
isAddNew: Boolean, // Only set if 'fake' item used as Add New button isAddNew: Boolean, // Only set if 'fake' item used as Add New button
}, },
@ -138,13 +139,6 @@ export default {
default: return undefined; default: return undefined;
} }
}, },
/* Get href for anchor, if not in edit mode, or opening in modal/ workspace */
hyperLinkHref() {
const nothing = '#';
if (this.isEditMode) return nothing;
const noAnchorNeeded = ['modal', 'workspace', 'clipboard'];
return noAnchorNeeded.includes(this.accumulatedTarget) ? nothing : this.url;
},
}, },
data() { data() {
return { return {
@ -167,20 +161,28 @@ export default {
itemOpened(e) { itemOpened(e) {
if (this.isEditMode) { if (this.isEditMode) {
// If in edit mode, open settings, and don't launch app // If in edit mode, open settings, and don't launch app
e.preventDefault();
this.openItemSettings(); this.openItemSettings();
return; return;
} }
if (e.altKey || this.accumulatedTarget === 'modal') { // For certain opening methods, prevent default and manually navigate
if (e.ctrlKey) {
e.preventDefault();
window.open(this.url, '_blank');
} else if (e.altKey || this.accumulatedTarget === 'modal') {
e.preventDefault(); e.preventDefault();
this.$emit('triggerModal', this.url); this.$emit('triggerModal', this.url);
} else if (this.accumulatedTarget === 'workspace') { } else if (this.accumulatedTarget === 'workspace') {
e.preventDefault();
router.push({ name: 'workspace', query: { url: this.url } }); router.push({ name: 'workspace', query: { url: this.url } });
} else if (this.accumulatedTarget === 'clipboard') { } else if (this.accumulatedTarget === 'clipboard') {
e.preventDefault();
navigator.clipboard.writeText(this.url); navigator.clipboard.writeText(this.url);
this.$toasted.show(this.$t('context-menus.item.copied-toast')); this.$toasted.show(this.$t('context-menus.item.copied-toast'));
} else {
this.$emit('itemClicked');
} }
// Emit event to clear search field, etc
this.$emit('itemClicked');
// Update the most/ last used ledger, for smart-sorting // Update the most/ last used ledger, for smart-sorting
if (!this.appConfig.disableSmartSort) { if (!this.appConfig.disableSmartSort) {
this.incrementMostUsedCount(this.id); this.incrementMostUsedCount(this.id);
@ -237,7 +239,12 @@ export default {
/* Pulls together all user options, returns URL + Get params for ping endpoint */ /* Pulls together all user options, returns URL + Get params for ping endpoint */
makeApiUrl() { makeApiUrl() {
const { const {
url, statusCheckUrl, statusCheckHeaders, statusCheckAllowInsecure, statusCheckAcceptCodes, url,
statusCheckUrl,
statusCheckHeaders,
statusCheckAllowInsecure,
statusCheckAcceptCodes,
statusCheckMaxRedirects,
} = this; } = this;
const encode = (str) => encodeURIComponent(str); const encode = (str) => encodeURIComponent(str);
this.statusResponse = undefined; this.statusResponse = undefined;
@ -251,9 +258,10 @@ export default {
// Deterimine if user disabled security // Deterimine if user disabled security
const enableInsecure = statusCheckAllowInsecure ? '&enableInsecure=true' : ''; const enableInsecure = statusCheckAllowInsecure ? '&enableInsecure=true' : '';
const acceptCodes = statusCheckAcceptCodes ? `&acceptCodes=${statusCheckAcceptCodes}` : ''; const acceptCodes = statusCheckAcceptCodes ? `&acceptCodes=${statusCheckAcceptCodes}` : '';
const maxRedirects = statusCheckMaxRedirects ? `&maxRedirects=${statusCheckMaxRedirects}` : '';
// Construct the full API endpoint's URL with GET params // Construct the full API endpoint's URL with GET params
return `${baseUrl}${serviceEndpoints.statusCheck}/${urlToCheck}` return `${baseUrl}${serviceEndpoints.statusCheck}/${urlToCheck}`
+ `${headers}${enableInsecure}${acceptCodes}`; + `${headers}${enableInsecure}${acceptCodes}${maxRedirects}`;
}, },
/* Checks if a given service is currently online */ /* Checks if a given service is currently online */
checkWebsiteStatus() { checkWebsiteStatus() {

View File

@ -45,11 +45,11 @@ export default {
return this.$store.getters.appConfig; return this.$store.getters.appConfig;
}, },
/* Determines the type of icon */ /* Determines the type of icon */
iconType: function iconType() { iconType() {
return this.determineImageType(this.icon); return this.determineImageType(this.icon);
}, },
/* Gets the icon path, dependent on icon type */ /* Gets the icon path, dependent on icon type */
iconPath: function iconPath() { iconPath() {
if (this.broken) return this.getFallbackIcon(); if (this.broken) return this.getFallbackIcon();
return this.getIconPath(this.icon, this.url); return this.getIconPath(this.icon, this.url);
}, },
@ -176,7 +176,7 @@ export default {
}, },
/* Fetches the path of local images, from Docker container */ /* Fetches the path of local images, from Docker container */
getLocalImagePath(img) { getLocalImagePath(img) {
return `${iconCdns.localPath}/${img}`; return `/${iconCdns.localPath}/${img}`;
}, },
/* Formats the URL for fetching the generative icons */ /* Formats the URL for fetching the generative icons */
getGenerativeIcon(url, cdn) { getGenerativeIcon(url, cdn) {

View File

@ -42,6 +42,7 @@
:statusCheckInterval="statusCheckInterval" :statusCheckInterval="statusCheckInterval"
:statusCheckAllowInsecure="item.statusCheckAllowInsecure" :statusCheckAllowInsecure="item.statusCheckAllowInsecure"
:statusCheckAcceptCodes="item.statusCheckAcceptCodes" :statusCheckAcceptCodes="item.statusCheckAcceptCodes"
:statusCheckMaxRedirects="item.statusCheckMaxRedirects"
@itemClicked="$emit('itemClicked')" @itemClicked="$emit('itemClicked')"
@triggerModal="triggerModal" @triggerModal="triggerModal"
:isAddNew="false" :isAddNew="false"
@ -151,7 +152,7 @@ export default {
return this.$store.state.editMode; return this.$store.state.editMode;
}, },
itemSize() { itemSize() {
return this.$store.getters.iconSize; return this.displayData.itemSize || this.$store.getters.iconSize;
}, },
sortOrder() { sortOrder() {
return this.displayData.sortBy || defaultSortOrder; return this.displayData.sortBy || defaultSortOrder;

View File

@ -19,6 +19,7 @@
:enableStatusCheck="shouldEnableStatusCheck(item.statusCheck)" :enableStatusCheck="shouldEnableStatusCheck(item.statusCheck)"
:statusCheckAllowInsecure="item.statusCheckAllowInsecure" :statusCheckAllowInsecure="item.statusCheckAllowInsecure"
:statusCheckAcceptCodes="item.statusCheckAcceptCodes" :statusCheckAcceptCodes="item.statusCheckAcceptCodes"
:statusCheckMaxRedirects="item.statusCheckMaxRedirects"
:statusCheckInterval="getStatusCheckInterval()" :statusCheckInterval="getStatusCheckInterval()"
@itemClicked="$emit('itemClicked')" @itemClicked="$emit('itemClicked')"
@triggerModal="triggerModal" @triggerModal="triggerModal"

View File

@ -5,15 +5,23 @@
@click="navVisible = !navVisible" @click="navVisible = !navVisible"
/> />
<nav id="nav" v-if="navVisible"> <nav id="nav" v-if="navVisible">
<router-link <!-- Render either router-link or anchor, depending if internal / external link -->
v-for="(link, index) in links" <template v-for="(link, index) in links">
:key="index" <router-link v-if="!isUrl(link.path)"
:to="link.path" :key="index"
:href="link.path" :to="link.path"
:target="isUrl(link.path) ? '_blank' : ''" class="nav-item"
rel="noopener noreferrer" >{{link.title}}
class="nav-item" </router-link>
>{{link.title}}</router-link> <a v-else
:key="index"
:href="link.path"
:target="determineTarget(link)"
class="nav-item"
rel="noopener noreferrer"
>{{link.title}}
</a>
</template>
</nav> </nav>
</div> </div>
</template> </template>
@ -43,6 +51,16 @@ export default {
return screenWidth && screenWidth < 600; return screenWidth && screenWidth < 600;
}, },
isUrl: (str) => new RegExp(/(http|https):\/\/(\S+)(:[0-9]+)?/).test(str), isUrl: (str) => new RegExp(/(http|https):\/\/(\S+)(:[0-9]+)?/).test(str),
determineTarget(link) {
if (!link.target) return '_blank';
switch (link.target) {
case 'sametab': return '_self';
case 'newtab': return '_blank';
case 'parent': return '_parent';
case 'top': return '_top';
default: return undefined;
}
},
}, },
}; };
</script> </script>

View File

@ -476,10 +476,13 @@ export default {
/* Returns users specified widget options, or empty object */ /* Returns users specified widget options, or empty object */
widgetOptions() { widgetOptions() {
const options = this.widget.options || {}; const options = this.widget.options || {};
const timeout = this.widget.timeout || 500;
const useProxy = this.appConfig.widgetsAlwaysUseProxy || !!this.widget.useProxy; const useProxy = this.appConfig.widgetsAlwaysUseProxy || !!this.widget.useProxy;
const updateInterval = this.widget.updateInterval !== undefined const updateInterval = this.widget.updateInterval !== undefined
? this.widget.updateInterval : null; ? this.widget.updateInterval : null;
return { useProxy, updateInterval, ...options }; return {
timeout, useProxy, updateInterval, ...options,
};
}, },
/* A unique string to reference the widget by */ /* A unique string to reference the widget by */
widgetRef() { widgetRef() {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="web-content" :id="id"> <div class="web-content" :id="id">
<iframe :src="url" /> <iframe :src="url" allow="fullscreen" />
</div> </div>
</template> </template>

View File

@ -14,8 +14,7 @@ export default {
credentials() { credentials() {
if (this.options.username && this.options.password) { if (this.options.username && this.options.password) {
const stringifiedUser = `${this.options.username}:${this.options.password}`; const stringifiedUser = `${this.options.username}:${this.options.password}`;
const headers = { Authorization: `Basic ${window.btoa(stringifiedUser)}` }; return { Authorization: `Basic ${window.btoa(stringifiedUser)}` };
return { headers };
} }
return null; return null;
}, },

View File

@ -106,8 +106,9 @@ const WidgetMixin = {
const CustomHeaders = options || null; const CustomHeaders = options || null;
const headers = this.useProxy const headers = this.useProxy
? { 'Target-URL': endpoint, CustomHeaders: JSON.stringify(CustomHeaders) } : CustomHeaders; ? { 'Target-URL': endpoint, CustomHeaders: JSON.stringify(CustomHeaders) } : CustomHeaders;
const timeout = this.options.timeout || 500;
const requestConfig = { const requestConfig = {
method, url, headers, data, method, url, headers, data, timeout,
}; };
// Make request // Make request
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -32,14 +32,16 @@ export default class ConfigAccumulator {
let usersAppConfig = defaultAppConfig; let usersAppConfig = defaultAppConfig;
if (localStorage[localStorageKeys.APP_CONFIG]) { if (localStorage[localStorageKeys.APP_CONFIG]) {
usersAppConfig = JSON.parse(localStorage[localStorageKeys.APP_CONFIG]); usersAppConfig = JSON.parse(localStorage[localStorageKeys.APP_CONFIG]);
} else if (appConfigFile !== {}) { } else if (Object.keys(appConfigFile).length > 0) {
usersAppConfig = appConfigFile; usersAppConfig = appConfigFile;
} }
// Some settings have their own local storage keys, apply them here // Some settings have their own local storage keys, apply them here
usersAppConfig.layout = localStorage[localStorageKeys.LAYOUT_ORIENTATION] usersAppConfig.layout = appConfigFile.layout
|| appConfigFile.layout || defaultLayout; || localStorage[localStorageKeys.LAYOUT_ORIENTATION]
usersAppConfig.iconSize = localStorage[localStorageKeys.ICON_SIZE] || defaultLayout;
|| appConfigFile.iconSize || defaultIconSize; usersAppConfig.iconSize = appConfigFile.iconSize
|| localStorage[localStorageKeys.ICON_SIZE]
|| defaultIconSize;
// Don't let users modify users locally // Don't let users modify users locally
if (appConfigFile.auth) usersAppConfig.auth = appConfigFile.auth; if (appConfigFile.auth) usersAppConfig.auth = appConfigFile.auth;
// All done, return final appConfig object // All done, return final appConfig object

View File

@ -36,6 +36,18 @@
}, },
"path": { "path": {
"type": "string" "type": "string"
},
"target": {
"title": "Opening Method",
"type": "string",
"enum": [
"newtab",
"sametab",
"parent",
"top"
],
"default": "newtab",
"description": "Where / how the item is opened when it's clicked"
} }
} }
} }
@ -789,11 +801,16 @@
"description": "Allows for running status checks on insecure content/ non-HTTPS apps. Prevents checks failing for non-SSL sites" "description": "Allows for running status checks on insecure content/ non-HTTPS apps. Prevents checks failing for non-SSL sites"
}, },
"statusCheckAcceptCodes": { "statusCheckAcceptCodes": {
"title": "Accepted HTTP Status Codes", "title": "Status Check - Accepted HTTP Codes",
"type": "string", "type": "string",
"default": "",
"description": "If your service's response code is anything other than 2xx, then you can opt to specify an alternative success code" "description": "If your service's response code is anything other than 2xx, then you can opt to specify an alternative success code"
}, },
"statusCheckMaxRedirects": {
"title": "Status Check - Max Redirects",
"type": "number",
"default": "0",
"description": "If your service redirects to another page, and you would like status checks to follow redirects, then specify the maximum number of redirects here"
},
"color": { "color": {
"title": "Custom Color", "title": "Custom Color",
"type": "string", "type": "string",
@ -820,6 +837,21 @@
"type": "string", "type": "string",
"description": "The type of widget to use, see docs for supported options" "description": "The type of widget to use, see docs for supported options"
}, },
"updateInterval": {
"title": "Update Interval",
"type": "number",
"description": "Specified in seconds. If set, widget data will be re-retched at this interval to display up-to-date data"
},
"timeout": {
"title": "Timeout",
"type": "number",
"description": "Specified in milliseconds. If set, request will timeout after the specified interval. Defaults to 500/ half a sec"
},
"useProxy": {
"title": "Use Proxy?",
"type": "boolean",
"description": "If set to true, request will be proxied through the backend. Requires the Node server to be running"
},
"options": { "options": {
"title": "Widget Options", "title": "Widget Options",
"type": "object", "type": "object",

View File

@ -14,7 +14,7 @@ class KeycloakAuth {
const { auth } = getAppConfig(); const { auth } = getAppConfig();
const { serverUrl, realm, clientId } = auth.keycloak; const { serverUrl, realm, clientId } = auth.keycloak;
const initOptions = { const initOptions = {
url: `${serverUrl}/auth`, realm, clientId, onLoad: 'login-required', url: `${serverUrl}`, realm, clientId, onLoad: 'login-required',
}; };
this.keycloakClient = Keycloak(initOptions); this.keycloakClient = Keycloak(initOptions);

View File

@ -32,6 +32,7 @@ export const LoadExternalTheme = function th() {
const preloadTheme = (href) => { const preloadTheme = (href) => {
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
link.type = 'text/css';
link.href = href; link.href = href;
document.head.appendChild(link); document.head.appendChild(link);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -201,7 +201,7 @@ module.exports = {
generativeFallback: 'https://evatar.io/{icon}', generativeFallback: 'https://evatar.io/{icon}',
localPath: './item-icons', localPath: './item-icons',
faviconName: 'favicon.ico', faviconName: 'favicon.ico',
homeLabIcons: 'https://raw.githubusercontent.com/WalkxCode/dashboard-icons/master/png/{icon}.png', homeLabIcons: 'https://raw.githubusercontent.com/walkxhub/dashboard-icons/master/png/{icon}.png',
homeLabIconsFallback: 'https://raw.githubusercontent.com/NX211/homer-icons/master/png/{icon}.png', homeLabIconsFallback: 'https://raw.githubusercontent.com/NX211/homer-icons/master/png/{icon}.png',
}, },
/* API endpoints for widgets that need to fetch external data */ /* API endpoints for widgets that need to fetch external data */

View File

@ -213,7 +213,7 @@ export default {
overflow: auto; overflow: auto;
@extend .scroll-bar; @extend .scroll-bar;
@include monitor-up { @include monitor-up {
max-width: 1400px; max-width: 85%;
} }
/* Options for alternate layouts, triggered by buttons */ /* Options for alternate layouts, triggered by buttons */