⤴ Rebased from master
This commit is contained in:
@@ -1,41 +1,40 @@
|
||||
<template>
|
||||
<modal :name="modalName" :resizable="true" width="55%" height="80%" classes="dashy-modal">
|
||||
<div class="about-modal">
|
||||
<router-link to="/about" class="title"><h2>App Info</h2></router-link>
|
||||
<router-link to="/about" class="title"><h2>{{ $t('app-info.title') }}</h2></router-link>
|
||||
<!-- Error Log -->
|
||||
<h3>Error Log</h3>
|
||||
<h3>{{ $t('app-info.error-log') }}</h3>
|
||||
<pre v-if="errorLog" class="logs"><code>{{ errorLog }}</code></pre>
|
||||
<p v-else>No recent errors detected :)</p>
|
||||
<p v-else>{{ $t('app-info.no-errors') }} :)</p>
|
||||
<hr />
|
||||
<!-- Getting Help -->
|
||||
<h3>Help & Support</h3>
|
||||
For getting support with running or configuring Dashy, see the <a href="https://github.com/Lissy93/dashy/discussions">Discussions</a>
|
||||
<h3>{{ $t('app-info.help-support') }}</h3>
|
||||
{{ $t('app-info.help-support-description') }} <a href="https://github.com/Lissy93/dashy/discussions">{{ $t('app-info.help-support-discussions') }}</a>
|
||||
<!-- Please help out :) -->
|
||||
<h3>Supporting Dashy</h3>
|
||||
For ways that you can get involved, check out the <a href="https://github.com/Lissy93/dashy/blob/master/docs/contributing.md">Contributing</a> page.
|
||||
<h3>{{ $t('app-info.support-dashy') }}</h3>
|
||||
{{ $t('app-info.support-dashy-description') }} <a href="https://github.com/Lissy93/dashy/blob/master/docs/contributing.md">{{ $t('app-info.support-dashy-link') }}</a>.
|
||||
<!-- Bug Reports -->
|
||||
<h3>Report a Bug</h3>
|
||||
If you think you've found a bug, then please <a href="https://github.com/Lissy93/dashy/issues/new/choose">raise an Issue</a>.
|
||||
<h3>{{ $t('app-info.report-bug') }}</h3>
|
||||
{{ $t('app-info.report-bug-description') }} <a href="https://github.com/Lissy93/dashy/issues/new/choose">{{ $t('app-info.report-bug-link') }}</a>.
|
||||
<!-- Source and Docs Links -->
|
||||
<h3>More Info</h3>
|
||||
Source: <a href="https://github.com/lissy93/dashy">github.com/lissy93/dashy</a><br>
|
||||
Documentation: <a href="https://dashy.to/docs">dashy.to/docs</a>
|
||||
<h3>{{ $t('app-info.more-info') }}</h3>
|
||||
{{ $t('app-info.source') }}: <a href="https://github.com/lissy93/dashy">github.com/lissy93/dashy</a><br>
|
||||
{{ $t('app-info.documentation') }}: <a href="https://dashy.to/docs">dashy.to/docs</a>
|
||||
<!-- Privacy & Security -->
|
||||
<h3>Privacy & Security</h3>
|
||||
For a break-down of how your data is managed by Dashy, see
|
||||
the <a href="https://github.com/Lissy93/dashy/blob/master/docs/privacy.md">Privacy Policy</a>.<br>
|
||||
For advise in securing your dashboard, you can reference the
|
||||
<a href="https://github.com/Lissy93/dashy/blob/master/docs/management.md">Management Docs</a>.<br>
|
||||
If you've found a potential security issue, report it following our
|
||||
<a href="https://github.com/Lissy93/dashy/blob/master/.github/SECURITY.md">Security Policy</a>
|
||||
<h3>{{ $t('app-info.privacy-and-security') }}</h3>
|
||||
{{ $t('app-info.privacy-and-security-l1') }} <a href="https://github.com/Lissy93/dashy/blob/master/docs/privacy.md">{{ $t('app-info.privacy-and-security-privacy-policy') }}</a>.<br>
|
||||
{{ $t('app-info.privacy-and-security-advice') }}
|
||||
<a href="https://github.com/Lissy93/dashy/blob/master/docs/management.md">{{ $t('app-info.privacy-and-security-advice-link') }}</a>.<br>
|
||||
{{ $t('app-info.privacy-and-security-security-issue') }}
|
||||
<a href="https://github.com/Lissy93/dashy/blob/master/.github/SECURITY.md">{{ $t('app-info.privacy-and-security-security-policy') }}</a>
|
||||
<!-- License -->
|
||||
<h3>License</h3>
|
||||
Licensed under <a href="https://github.com/Lissy93/dashy/blob/master/LICENSE">MIT X11</a>.
|
||||
<h3>{{ $t('app-info.license') }}</h3>
|
||||
{{ $t('app-info.license-under') }} <a href="https://github.com/Lissy93/dashy/blob/master/LICENSE">MIT X11</a>.
|
||||
Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © 2021.<br>
|
||||
For licenses for third-party modules, please see <a href="https://github.com/Lissy93/dashy/blob/master/.github/LEGAL.md">Legal</a>.<br>
|
||||
For the full list of contributors and thanks, see <a href="https://github.com/Lissy93/dashy/blob/master/docs/credits.md">Credits</a>.
|
||||
{{ $t('app-info.licence-third-party') }} <a href="https://github.com/Lissy93/dashy/blob/master/.github/LEGAL.md">{{ $t('app-info.licence-third-party-link') }}</a>.<br>
|
||||
{{ $t('app-info.list-contributors') }} <a href="https://github.com/Lissy93/dashy/blob/master/docs/credits.md">{{ $t('app-info.list-contributors-link') }}</a>.
|
||||
<!-- App Version -->
|
||||
<h3>Version</h3>
|
||||
<h3>{{ $t('app-info.version') }}</h3>
|
||||
<AppVersion class="app-version" />
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ $t('cloud-sync.intro-l2') }}
|
||||
<br>
|
||||
{{ $t('cloud-sync.intro-l3') }}
|
||||
<a href="https://github.com/Lissy93/dashy/blob/master/docs/backup-restore.md">docs</a>
|
||||
<a href="https://github.com/Lissy93/dashy/blob/master/docs/backup-restore.md">{{ $t('cloud-sync.intro-docs') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Create or update a backup form -->
|
||||
|
||||
@@ -54,12 +54,8 @@
|
||||
<AppVersion />
|
||||
</div>
|
||||
<!-- Display note if Config disabled, or if on mobile -->
|
||||
<p v-if="!enableConfig" class="config-disabled-note">
|
||||
Some configuration features have been disabled by your administrator
|
||||
</p>
|
||||
<p class="small-screen-note" style="display: none;">
|
||||
You are using a very small screen, and some screens in this menu may not be optimal
|
||||
</p>
|
||||
<p v-if="!enableConfig" class="config-disabled-note">{{ $t('config.disabled-note') }}</p>
|
||||
<p class="small-screen-note" style="display: none;">{{ $t('config.small-screen-note') }}</p>
|
||||
<div class="config-note">
|
||||
<span>{{ $t('config.backup-note') }}</span>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="css-editor-outer">
|
||||
<!-- Add raw custom CSS -->
|
||||
<div class="style-section css-wrapper">
|
||||
<h3>Custom CSS</h3>
|
||||
<h3>{{ $t('config.custom-css.title') }}</h3>
|
||||
<textarea class="css-editor" v-model="customCss" />
|
||||
<Button class="save-button" :click="save">{{ $t('config.css-save-btn') }}</Button>
|
||||
<p class="quick-note">
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<!-- Theme Selector -->
|
||||
<div class="style-section base-theme-wrapper">
|
||||
<h3>Base Theme</h3>
|
||||
<h3>{{ $t('config.custom-css.base-theme') }}</h3>
|
||||
<ThemeSelector :hidePallete="true" />
|
||||
</div>
|
||||
<!-- UI color configurator -->
|
||||
|
||||
@@ -70,7 +70,7 @@ export default {
|
||||
const filename = 'dashy_conf.yml';
|
||||
const config = this.convertJsonToYaml();
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', `data:text/plain;charset=utf-8, ${encodeURIComponent(config)}`);
|
||||
element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(config)}`);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
classes="dashy-modal">
|
||||
<div slot="top-right" @click="hide()">Close</div>
|
||||
<a @click="hide()" class="close-button" title="Close">x</a>
|
||||
<iframe v-if="url" :src="url" @keydown.esc="close" class="frame" allow="fullscreen" />
|
||||
<iframe v-if="url" :src="url" @keydown.esc="close" class="frame"
|
||||
allow="fullscreen; clipboard-write" />
|
||||
<div v-else class="no-url">No URL Specified</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
@@ -14,16 +14,16 @@
|
||||
:style="customStyle"
|
||||
>
|
||||
<!-- Item Text -->
|
||||
<div :class="`tile-title ${!item.icon? 'bounce no-icon': ''}`" :id="`tile-${item.id}`" >
|
||||
<div :class="`tile-title ${!itemIcon? 'bounce no-icon': ''}`" :id="`tile-${item.id}`" >
|
||||
<span class="text">{{ item.title }}</span>
|
||||
<p class="description">{{ item.description }}</p>
|
||||
</div>
|
||||
<!-- Item Icon -->
|
||||
<Icon :icon="item.icon" :url="item.url" :size="size" :color="item.color"
|
||||
<Icon :icon="itemIcon" :url="item.url" :size="size" :color="item.color"
|
||||
v-bind:style="customStyles" class="bounce" />
|
||||
<!-- Small icon, showing opening method on hover -->
|
||||
<ItemOpenMethodIcon class="opening-method-icon"
|
||||
:isSmall="!item.icon || size === 'small'"
|
||||
:isSmall="!itemIcon || size === 'small'"
|
||||
:openingMethod="accumulatedTarget" position="bottom right"
|
||||
:hotkey="item.hotkey" />
|
||||
<!-- Status indicator dot (if enabled) showing weather service is available -->
|
||||
@@ -65,7 +65,6 @@ import MoveItemTo from '@/components/InteractiveEditor/MoveItemTo';
|
||||
import ContextMenu from '@/components/LinkItems/ItemContextMenu';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import ItemMixin from '@/mixins/ItemMixin';
|
||||
// import { targetValidator } from '@/utils/ConfigHelpers';
|
||||
import EditModeIcon from '@/assets/interface-icons/interactive-editor-edit-mode.svg';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
|
||||
@@ -89,6 +88,10 @@ export default {
|
||||
EditModeIcon,
|
||||
},
|
||||
computed: {
|
||||
/* Returns either item.icon, or appConfig.defaultIcon, or null */
|
||||
itemIcon() {
|
||||
return this.item.icon || this.$store.getters.appConfig?.defaultIcon;
|
||||
},
|
||||
makeColumnCount() {
|
||||
if ((this.sectionDisplayData || {}).itemCountX) return this.sectionDisplayData.itemCountX;
|
||||
if (this.sectionWidth < 380) return 1;
|
||||
@@ -101,8 +104,7 @@ export default {
|
||||
/* Based on item props, adjust class names */
|
||||
makeClassList() {
|
||||
const { isAddNew, isEditMode, size } = this;
|
||||
const { icon } = this.item;
|
||||
return `size-${size} ${!icon ? 'short' : ''} `
|
||||
return `size-${size} ${!this.itemIcon ? 'short' : ''} `
|
||||
+ `${isAddNew ? 'add-new' : ''} ${isEditMode ? 'is-edit-mode' : ''}`;
|
||||
},
|
||||
/* Used by certain themes (material), to show animated CSS icon */
|
||||
|
||||
@@ -202,7 +202,7 @@ export default {
|
||||
/* For a given URL, return the hostname only. Used for favicon and generative icons */
|
||||
getHostName(url) {
|
||||
try {
|
||||
return new URL(url).hostname.split('.').slice(-2).join('.');
|
||||
return new URL(url).hostname;
|
||||
} catch (e) {
|
||||
ErrorHandler('Unable to format URL');
|
||||
return url;
|
||||
|
||||
@@ -389,7 +389,9 @@ export default {
|
||||
justify-content: space-around;
|
||||
.widget-base {
|
||||
min-width: 10rem;
|
||||
width: stretch;
|
||||
width: -webkit-fill-available;
|
||||
width: -moz-available;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
subItemTooltip() {
|
||||
return this.title;
|
||||
return this.item.title;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
© {{defaultInfo.date}}.
|
||||
Get the <a :href="defaultInfo.repoUrl">Source Code</a>.
|
||||
</span>
|
||||
<!-- Config info -->
|
||||
<span class="path-to-config">
|
||||
<span>
|
||||
Using: {{ $store.state.currentConfigId || 'Default Config' }}
|
||||
{{ $t('footer.dev-by') }} <a :href="authorUrl">{{authorName}}</a>.
|
||||
{{ $t('footer.licensed-under') }} <a :href="licenseUrl">{{license}}</a>
|
||||
{{ showCopyright? '©': '' }} {{date}}.
|
||||
{{ $t('footer.get-the') }} <a :href="repoUrl">{{ $t('footer.source-code') }}</a>.
|
||||
</span>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div id="loading" v-if="isLoading" :class="c" @click="c = 'hide'">
|
||||
<h2>Dashy</h2>
|
||||
<div class="inner-container">
|
||||
<p>Loading</p>
|
||||
<p>{{ $t('splash-screen.loading')}}</p>
|
||||
<span class="dots-cont">
|
||||
<span class="dot dot-1"></span>
|
||||
<span class="dot dot-2"></span>
|
||||
|
||||
@@ -39,6 +39,11 @@ export default {
|
||||
showSeconds() {
|
||||
return !this.options.hideSeconds;
|
||||
},
|
||||
use12Hour() {
|
||||
if (typeof this.options.use12Hour === 'boolean') return this.options.use12Hour;
|
||||
// this is the default, it gets computed by the DateTimeFormat implementation
|
||||
return Intl.DateTimeFormat(this.timeFormat, { timeZone: this.timeZone, hour: 'numeric' }).resolvedOptions().hour12 ?? false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
@@ -52,12 +57,17 @@ export default {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
...(this.showSeconds && { second: 'numeric' }),
|
||||
...(this.use12Hour && { hourCycle: 'h12' }),
|
||||
}).format();
|
||||
},
|
||||
/* Get and format the date */
|
||||
setDate() {
|
||||
this.date = new Date().toLocaleDateString(this.timeFormat, {
|
||||
weekday: 'long', day: 'numeric', year: 'numeric', month: 'short',
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
timeZone: this.timeZone,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
74
src/components/Widgets/CustomSearch.vue
Normal file
74
src/components/Widgets/CustomSearch.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="custom-search">
|
||||
<input type="text" v-model="query"
|
||||
@keyup.enter="search(defaultEngine)"
|
||||
@keyup.stop @keydown.stop
|
||||
:placeholder="placeholder">
|
||||
<div class="buttons">
|
||||
<button
|
||||
v-for="(engine, key) in engines" :key="key"
|
||||
v-on:click="search(engine)">
|
||||
{{ engine.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
placeholder() {
|
||||
return this.options.placeholder || '';
|
||||
},
|
||||
engines() {
|
||||
return this.options.engines || [];
|
||||
},
|
||||
defaultEngine() {
|
||||
return this.engines[0];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
search(engine) {
|
||||
if (engine !== undefined && this.query !== '') {
|
||||
window.open(engine.url + this.query, '_blank');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.custom-search {
|
||||
font-size: 1.2rem;
|
||||
input {
|
||||
width: 80%;
|
||||
margin: 1rem 10%;
|
||||
padding: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.buttons {
|
||||
text-align:center;
|
||||
button{
|
||||
margin: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
color: var(--item-text-color);
|
||||
background: var(--item-background);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -92,7 +92,7 @@ export default {
|
||||
},
|
||||
endpoint() {
|
||||
return `${widgetApiEndpoints.cveVulnerabilities}?${this.sortBy}${this.limit}`
|
||||
+ `${this.minScore}${this.vendorId}${this.hasExploit}`;
|
||||
+ `${this.minScore}${this.vendorId}${this.productId}${this.hasExploit}`;
|
||||
},
|
||||
proxyReqEndpoint() {
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
|
||||
238
src/components/Widgets/DroneCi.vue
Normal file
238
src/components/Widgets/DroneCi.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="droneci-builds-wrapper" v-if="builds">
|
||||
<div
|
||||
class="build-row"
|
||||
v-for="build in builds" :key="build.id"
|
||||
v-tooltip="infoTooltip(build)"
|
||||
>
|
||||
<div class="status">
|
||||
<p :class="build.build.status">{{ build.build.status | formatStatus }}</p>
|
||||
<span v-if="build.build.status == 'running'">
|
||||
{{ build.build.started*1000 | formatTimeAgo }} ago
|
||||
</span>
|
||||
<span v-else-if="build.build.status != 'pending' ">
|
||||
{{ formatBuildDuration(build) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ build.build.created*1000 | formatTimeAgo }} ago
|
||||
</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="build-name">
|
||||
{{ build.name }}
|
||||
<a
|
||||
class="droneci-build-number"
|
||||
:href="build.baseurl + '/' + build.slug + '/' +build.build.number"
|
||||
target="_blank"
|
||||
>{{ build.build.number }}</a>
|
||||
</div>
|
||||
<div class="build-desc">
|
||||
<span class="droneci-extra">
|
||||
<template v-if="build.build.event == 'pull_request'">
|
||||
<a
|
||||
:href="build.build.link"
|
||||
target="_blank"
|
||||
class="droneci-extra-info"
|
||||
>#{{ formatPrId(build.build.link) }}</a> to
|
||||
</template>
|
||||
<template v-else-if="build.build.event == 'push'">
|
||||
<a
|
||||
:href="build.build.link"
|
||||
target="_blank"
|
||||
class="droneci-extra-info"
|
||||
>push</a> to
|
||||
</template>
|
||||
<a
|
||||
:href="build.git_http_url"
|
||||
target="_blank"
|
||||
class="droneci-extra-info"
|
||||
>
|
||||
{{ build.build.target }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { getTimeAgo, getTimeDifference, timestampToDateTime } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
builds: null,
|
||||
};
|
||||
},
|
||||
filters: {
|
||||
formatStatus(status) {
|
||||
let symbol = '';
|
||||
if (status === 'success') symbol = '✔';
|
||||
if (status === 'failure' || status === 'error' || status === 'killed') symbol = '✘';
|
||||
if (status === 'running') symbol = '❖';
|
||||
if (status === 'skipped') symbol = '↠';
|
||||
return `${symbol}`;
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
return timestampToDateTime(timestamp);
|
||||
},
|
||||
formatTimeAgo(timestamp) {
|
||||
return getTimeAgo(timestamp);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/* API endpoint, either for self-hosted or managed instance */
|
||||
endpointBuilds() {
|
||||
if (!this.options.host) this.error('drone.ci Host is required');
|
||||
return `${this.options.host}/api/user/builds`;
|
||||
},
|
||||
endpointRepoInfo() {
|
||||
if (!this.options.host) this.error('drone.ci Host is required');
|
||||
return `${this.options.host}/api/repos/${this.options.repo}`;
|
||||
},
|
||||
endpointRepoBuilds() {
|
||||
if (!this.options.host) this.error('drone.ci Host is required');
|
||||
return `${this.options.host}/api/repos/${this.options.repo}/builds`;
|
||||
},
|
||||
repo() {
|
||||
if (this.options.repo) return this.options.repo;
|
||||
return false;
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) {
|
||||
this.error('An API key is required, please see the docs for more info');
|
||||
}
|
||||
return this.options.apiKey;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Fetch new data, configured by updateInterval */
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.fetchData();
|
||||
this.finishLoading();
|
||||
},
|
||||
/* Make GET request to Drone CI API endpoint */
|
||||
fetchData() {
|
||||
const authHeaders = { Authorization: `Bearer ${this.apiKey}` };
|
||||
if (this.repo !== false) {
|
||||
this.makeRequest(this.endpointRepoInfo, authHeaders).then(
|
||||
(repoInfo) => {
|
||||
this.makeRequest(this.endpointRepoBuilds, authHeaders).then(
|
||||
(buildInfo) => {
|
||||
this.processRepoBuilds(repoInfo, buildInfo);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.makeRequest(this.endpointBuilds, authHeaders).then(
|
||||
(response) => { this.processBuilds(response); },
|
||||
);
|
||||
}
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processBuilds(data) {
|
||||
const results = data.slice(0, this.options.limit)
|
||||
.map((obj) => ({ ...obj, baseurl: this.options.host }));
|
||||
this.builds = results;
|
||||
},
|
||||
processRepoBuilds(repo, builds) {
|
||||
const results = builds.slice(0, this.options.limit)
|
||||
.map((obj) => ({ build: { ...obj }, baseurl: this.options.host, ...repo }));
|
||||
this.builds = results;
|
||||
},
|
||||
infoTooltip(build) {
|
||||
const content = `<b>Trigger:</b> ${build.build.event} by ${build.build.trigger}<br>`
|
||||
+ `<b>Repo:</b> ${build.slug}<br>`
|
||||
+ `<b>Branch:</b> ${build.build.target}<br>`;
|
||||
return {
|
||||
content, html: true, trigger: 'hover focus', delay: 250, classes: 'build-info-tt',
|
||||
};
|
||||
},
|
||||
formatPrId(link) {
|
||||
return link.split('/').pop();
|
||||
},
|
||||
formatBuildDuration(build) {
|
||||
return getTimeDifference(build.build.started * 1000, build.build.finished * 1000);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.droneci-builds-wrapper {
|
||||
color: var(--widget-text-color);
|
||||
.build-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2.5fr;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
.status {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--info);
|
||||
&.success { color: var(--success); }
|
||||
&.failure { color: var(--danger); }
|
||||
&.error { color: var(--danger); }
|
||||
&.running { color: var(--neutral); }
|
||||
}
|
||||
span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
.info {
|
||||
div.build-name {
|
||||
margin: 0.25rem 0;
|
||||
font-weight: bold;
|
||||
color: var(--widget-text-color);
|
||||
a, a:hover, a:visited, a:active {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.droneci-build-number::before {
|
||||
content: "#";
|
||||
}
|
||||
}
|
||||
div.build-desc {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--widget-text-color);
|
||||
opacity: var(--dimming-factor);
|
||||
a, a:hover, a:visited, a:active {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.droneci-extra {
|
||||
.droneci-extra-info {
|
||||
margin: 0.25em;
|
||||
padding: 0em 0.25em;
|
||||
background: var(--item-background);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.build-info-tt {
|
||||
min-width: 20rem;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="glances-temp-wrapper" v-if="tempData">
|
||||
<div class="temp-row" v-for="sensor in tempData" :key="sensor.label">
|
||||
<p class="label">{{ sensor.label | formatLbl }}</p>
|
||||
<p :class="`temp range-${sensor.color}`">{{ sensor.value | formatVal }}</p>
|
||||
<p :class="`temp range-${sensor.color}`">{{ sensor.value | formatVal(sensor.unit) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,7 +10,7 @@
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import GlancesMixin from '@/mixins/GlancesMixin';
|
||||
import { capitalize, fahrenheitToCelsius } from '@/utils/MiscHelpers';
|
||||
import { capitalize, celsiusToFahrenheit, fahrenheitToCelsius } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin, GlancesMixin],
|
||||
@@ -29,28 +29,105 @@ export default {
|
||||
formatLbl(lbl) {
|
||||
return capitalize(lbl);
|
||||
},
|
||||
formatVal(val) {
|
||||
return `${Math.round(val)}°C`;
|
||||
formatVal(val, unit) {
|
||||
switch (unit) {
|
||||
case 'R':
|
||||
return `${Math.round(val)} rpm`;
|
||||
case '%':
|
||||
return `${Math.round(val)}%`;
|
||||
default:
|
||||
return `${Math.round(val)}°${unit}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getDesiredUnits() {
|
||||
return this.options.units ?? 'C';
|
||||
},
|
||||
getDisplayValue(rawValue, units) {
|
||||
const desiredUnits = this.getDesiredUnits();
|
||||
if (units === desiredUnits) {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
return desiredUnits === 'C'
|
||||
? fahrenheitToCelsius(rawValue)
|
||||
: celsiusToFahrenheit(rawValue);
|
||||
},
|
||||
getCelsiusValue(rawValue, units) {
|
||||
if (units !== 'F' && units !== 'C') {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
return units === 'C' ? rawValue : fahrenheitToCelsius(rawValue);
|
||||
},
|
||||
getFahrenheitValue(rawValue, units) {
|
||||
if (units !== 'F' && units !== 'C') {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
return units === 'F' ? rawValue : celsiusToFahrenheit(rawValue);
|
||||
},
|
||||
getTempColor(temp) {
|
||||
if (temp <= 50) return 'green';
|
||||
if (temp > 50 && temp < 75) return 'yellow';
|
||||
if (temp >= 75) return 'red';
|
||||
return 'grey';
|
||||
},
|
||||
getPercentageColor(percentage) {
|
||||
if (percentage < 20) return 'red';
|
||||
if (percentage < 50) return 'orange';
|
||||
if (percentage < 75) return 'yellow';
|
||||
return 'green';
|
||||
},
|
||||
processData(sensorData) {
|
||||
const results = [];
|
||||
sensorData.forEach((sensor) => {
|
||||
const tempC = sensor.unit === 'F' ? fahrenheitToCelsius(sensor.value) : sensor.value;
|
||||
results.push({
|
||||
label: sensor.label,
|
||||
value: tempC,
|
||||
color: this.getTempColor(tempC),
|
||||
});
|
||||
});
|
||||
this.tempData = results;
|
||||
this.tempData = sensorData.map(sensor => {
|
||||
switch (sensor.unit) {
|
||||
case 'F':
|
||||
case 'C':
|
||||
return this.processTemperatureSensor(sensor);
|
||||
case 'R':
|
||||
return this.processFanSensor(sensor);
|
||||
case '%':
|
||||
return this.processBatterySensor(sensor);
|
||||
default:
|
||||
// Justification: This is a recoverable error that developers
|
||||
// should nevertheless be warned about.
|
||||
// eslint-disable-next-line
|
||||
console.warn('Unrecognized unit', sensor.unit);
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
},
|
||||
processBatterySensor({ label, unit, value }) {
|
||||
const color = this.getPercentageColor(value);
|
||||
return {
|
||||
color,
|
||||
label,
|
||||
unit,
|
||||
value,
|
||||
};
|
||||
},
|
||||
processFanSensor({ label, unit, value }) {
|
||||
return {
|
||||
color: 'grey',
|
||||
label,
|
||||
unit,
|
||||
value,
|
||||
};
|
||||
},
|
||||
processTemperatureSensor({ label, unit, value: originalValue }) {
|
||||
const celsiusValue = this.getCelsiusValue(originalValue, unit);
|
||||
const color = this.getTempColor(celsiusValue);
|
||||
const displayValue = this.getDisplayValue(originalValue, unit);
|
||||
const displayUnits = this.getDesiredUnits();
|
||||
|
||||
return {
|
||||
color,
|
||||
label,
|
||||
unit: displayUnits,
|
||||
value: displayValue,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,9 +43,6 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
this.makeRequest(this.endpoint).then(this.processData);
|
||||
},
|
||||
processData(diskData) {
|
||||
this.disks = diskData;
|
||||
},
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<div class="health-checks-wrapper" v-if="crons">
|
||||
<div
|
||||
class="cron-row"
|
||||
v-for="cron in crons" :key="cron.id"
|
||||
v-tooltip="pingTimeTooltip(cron)"
|
||||
<template
|
||||
v-for="cron in crons"
|
||||
>
|
||||
<div class="status">
|
||||
<div class="status" v-bind:key="cron.id + 'status'">
|
||||
<p :class="cron.status">{{ cron.status | formatStatus }}</p>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div
|
||||
class="info"
|
||||
v-tooltip="pingTimeTooltip(cron)"
|
||||
v-bind:key="cron.id + 'info'"
|
||||
>
|
||||
<p class="cron-name">{{ cron.name }}</p>
|
||||
<p class="cron-desc">{{ cron.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -35,6 +37,8 @@ export default {
|
||||
if (status === 'up') symbol = '✔';
|
||||
if (status === 'down') symbol = '✘';
|
||||
if (status === 'new') symbol = '❖';
|
||||
if (status === 'paused') symbol = '⏸';
|
||||
if (status === 'running') symbol = '▶';
|
||||
return `${symbol} ${capitalize(status)}`;
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
@@ -51,6 +55,9 @@ export default {
|
||||
if (!this.options.apiKey) {
|
||||
this.error('An API key is required, please see the docs for more info');
|
||||
}
|
||||
if (typeof this.options.apiKey === 'string') {
|
||||
return [this.options.apiKey];
|
||||
}
|
||||
return this.options.apiKey;
|
||||
},
|
||||
},
|
||||
@@ -58,14 +65,18 @@ export default {
|
||||
/* Make GET request to CoinGecko API endpoint */
|
||||
fetchData() {
|
||||
this.overrideProxyChoice = true;
|
||||
const authHeaders = { 'X-Api-Key': this.apiKey };
|
||||
this.makeRequest(this.endpoint, authHeaders).then(
|
||||
(response) => { this.processData(response); },
|
||||
);
|
||||
const results = [];
|
||||
this.apiKey.forEach((key) => {
|
||||
const authHeaders = { 'X-Api-Key': key };
|
||||
this.makeRequest(this.endpoint, authHeaders).then(
|
||||
(response) => { this.processData(response, results); },
|
||||
);
|
||||
});
|
||||
results.sort((a, b) => ((a.name > b.name) ? 1 : -1));
|
||||
this.crons = results;
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
const results = [];
|
||||
processData(data, results) {
|
||||
data.checks.forEach((cron) => {
|
||||
results.push({
|
||||
id: cron.slug,
|
||||
@@ -78,7 +89,7 @@ export default {
|
||||
url: this.makeUrl(cron.unique_key),
|
||||
});
|
||||
});
|
||||
this.crons = results;
|
||||
return results;
|
||||
},
|
||||
makeUrl(cronId) {
|
||||
const base = this.options.host || 'https://healthchecks.io';
|
||||
@@ -99,40 +110,40 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.health-checks-wrapper {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
color: var(--widget-text-color);
|
||||
.cron-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
.status {
|
||||
min-width: 5rem;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.25rem 0;
|
||||
.status {
|
||||
min-width: 5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--info);
|
||||
&.up { color: var(--success); }
|
||||
&.down { color: var(--danger); }
|
||||
&.new { color: var(--widget-text-color); }
|
||||
&.running { color: var(--warning); }
|
||||
&.paused { color: var(--info); }
|
||||
}
|
||||
}
|
||||
.info {
|
||||
p.cron-name {
|
||||
margin: 0.25rem 0;
|
||||
font-weight: bold;
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--info);
|
||||
&.up { color: var(--success); }
|
||||
&.down { color: var(--danger); }
|
||||
&.new { color: var(--neutral); }
|
||||
}
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
.info {
|
||||
p.cron-name {
|
||||
margin: 0.25rem 0;
|
||||
font-weight: bold;
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
p.cron-desc {
|
||||
margin: 0;
|
||||
color: var(--widget-text-color);
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
p.cron-desc {
|
||||
margin: 0;
|
||||
color: var(--widget-text-color);
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:src="frameUrl"
|
||||
:id="frameId"
|
||||
title="Iframe Widget"
|
||||
allow="fullscreen; clipboard-write"
|
||||
:style="frameHeight ? `height: ${frameHeight}px` : ''"
|
||||
/>
|
||||
</div>
|
||||
@@ -15,6 +16,9 @@ import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
data: () => ({
|
||||
updateCount: 0,
|
||||
}),
|
||||
computed: {
|
||||
/* Gets users specified URL to load into the iframe */
|
||||
frameUrl() {
|
||||
@@ -23,7 +27,7 @@ export default {
|
||||
this.error('Iframe widget expects a URL');
|
||||
return null;
|
||||
}
|
||||
return usersChoice;
|
||||
return `${usersChoice}${this.updatePathParam}`;
|
||||
},
|
||||
frameHeight() {
|
||||
return this.options.frameHeight;
|
||||
@@ -32,11 +36,16 @@ export default {
|
||||
frameId() {
|
||||
return `iframe-${btoa(this.frameUrl || 'empty').substring(0, 16)}`;
|
||||
},
|
||||
/* Generate a URL param, to be updated in order to re-fetch image */
|
||||
updatePathParam() {
|
||||
return this.updateCount ? `#dashy-update-${this.updateCount}` : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Refreshes iframe contents, called by parent */
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.updateCount += 1;
|
||||
(document.getElementById(this.frameId) || {}).src = this.frameUrl;
|
||||
this.finishLoading();
|
||||
},
|
||||
@@ -48,7 +57,7 @@ export default {
|
||||
.iframe-widget {
|
||||
iframe {
|
||||
width: 100%;
|
||||
min-height: 240px;
|
||||
min-height: 80px;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="image-widget">
|
||||
<img :src="imagePath" class="embedded-image" />
|
||||
<img :src="imagePath" :style="imageDimensions" class="embedded-image" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,10 +9,46 @@ import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
data: () => ({
|
||||
updateCount: 0,
|
||||
}),
|
||||
computed: {
|
||||
/* The path to image to render */
|
||||
imagePath() {
|
||||
if (!this.options.imagePath) this.error('You must specify an imagePath');
|
||||
return this.options.imagePath;
|
||||
return `${this.options.imagePath}${this.updatePathParam}`;
|
||||
},
|
||||
/* If set, apply users specified image dimensions */
|
||||
imageDimensions() {
|
||||
// Skip if neither set
|
||||
if (!this.options.imageWidth && !this.options.imageHeight) return null;
|
||||
// Apply correct units to input val, if needed
|
||||
const makeDimensionsUnit = (userVal) => {
|
||||
if (!userVal) { // Nothing set, use auto
|
||||
return 'auto';
|
||||
} else if (!Number.isNaN(Number(userVal))) { // Number set, add px
|
||||
return `${userVal}px`;
|
||||
} else { // Value is string, likely already includes units
|
||||
return userVal;
|
||||
}
|
||||
};
|
||||
// Return CSS values for width and height
|
||||
return `
|
||||
width: ${makeDimensionsUnit(this.options.imageWidth)};
|
||||
height: ${makeDimensionsUnit(this.options.imageHeight)};
|
||||
`;
|
||||
},
|
||||
/* Generate a URL param, to be updated in order to re-fetch image */
|
||||
updatePathParam() {
|
||||
return this.updateCount ? `#dashy-update-${this.updateCount}` : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* In order to re-fetch the image, we much update the URL with an arbitrary hash */
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.updateCount += 1;
|
||||
this.finishLoading();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
102
src/components/Widgets/Linkding.vue
Normal file
102
src/components/Widgets/Linkding.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="linkding-outer-wrapper">
|
||||
<div class="linkding-wrapper" v-if="links">
|
||||
<ul>
|
||||
<li
|
||||
v-for="link in links"
|
||||
v-bind:key="link.id"
|
||||
class="lingkding-link"
|
||||
>
|
||||
<a :href="link.url" target="_blank">
|
||||
<span class="linktext" v-tooltip="link.description">{{link.title}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
links: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
endpoint() {
|
||||
if (!this.options.host) this.error('linkgding Host is required');
|
||||
return `${this.options.host}/api/bookmarks`;
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('linkgding apiKey is required');
|
||||
return this.options.apiKey;
|
||||
},
|
||||
filtertags() {
|
||||
return this.options.tags;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.fetchData();
|
||||
this.finishLoading();
|
||||
},
|
||||
fetchData() {
|
||||
const authHeaders = { Authorization: `Token ${this.apiKey}` };
|
||||
this.makeRequest(this.endpoint, authHeaders).then(
|
||||
(response) => { this.processData(response); },
|
||||
);
|
||||
},
|
||||
processData(data) {
|
||||
const self = this;
|
||||
const fltr = (entry) => {
|
||||
if (self.filtertags === null) return true;
|
||||
for (let i = 0; i < self.filtertags.length; i += 1) {
|
||||
if (entry.tag_names.includes(self.filtertags[i])) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
this.links = data.results.filter(
|
||||
entry => fltr(entry),
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.linkdign-wrapper {
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.linkding-wrapper {
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
color: var(--widget-text-color);
|
||||
li {
|
||||
opacity: var(--dimming-factor);
|
||||
a, a:hover, a:visited, a:active {
|
||||
font-weight: bold;
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
span.linktext {
|
||||
color: var(--widget-text-color);
|
||||
}
|
||||
padding-top:0.2em;
|
||||
padding-bottom:0.2em;
|
||||
&:before
|
||||
{
|
||||
content: '🔗';
|
||||
margin: 0 0.7em; /* any design */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
353
src/components/Widgets/Mvg.vue
Normal file
353
src/components/Widgets/Mvg.vue
Normal file
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="mvg-wrapper" v-if="departures">
|
||||
<template
|
||||
v-for="departure in departures"
|
||||
>
|
||||
<div class="departure" v-bind:key="departure.key" v-tooltip="mvgTooltipDeparture(departure)">
|
||||
<span :class="{live: departure.live}">
|
||||
{{ departure.realtimeDepartureTime | formatDepartureTime }}
|
||||
</span>
|
||||
</div>
|
||||
<div class='line'
|
||||
v-bind:key="departure.key + 'line'"
|
||||
>
|
||||
<div
|
||||
class="transport"
|
||||
:class="['type-' + departure.transportType,
|
||||
'line-' + departure.label,
|
||||
]"
|
||||
>{{ departure.label }}</div>
|
||||
<div
|
||||
class='destination'
|
||||
v-tooltip="mvgTooltipDestination(departure)"
|
||||
:class="{cancelled: departure.cancelled}">{{ departure.destination }}</div>
|
||||
<span class="delay"
|
||||
:class="{'has-delay': departure.realtimeDepartureTime > departure.plannedDepartureTime}"
|
||||
>{{ Math.max(0,
|
||||
(departure.realtimeDepartureTime - departure.plannedDepartureTime)/60000) }}</span>
|
||||
<span class="occupancy"
|
||||
:class="'occupancy-' + departure.occupancy"
|
||||
v-if="departure.occupancy != 'UNKNOWN'"
|
||||
v-tooltip="departure.occupancy"
|
||||
>■</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||
import { timestampToTime } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
departures: null,
|
||||
locationSearch: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (!this.isLocationId) {
|
||||
this.makeRequest(this.endpointLocation).then(
|
||||
(response) => {
|
||||
const stations = response.filter((r) => r.type === 'STATION');
|
||||
if (stations.length > 0) {
|
||||
this.location = stations[0].globalId;
|
||||
this.fetchData();
|
||||
} else {
|
||||
this.error('Cannot find station for specified string');
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.location = this.options.location;
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDepartureTime(timestamp) {
|
||||
const msDifference = new Date(timestamp).getTime() - new Date().getTime();
|
||||
const diff = Math.max(0, Math.round(msDifference / 60000));
|
||||
return diff;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isLocationId() {
|
||||
if (!this.options.location) {
|
||||
this.error('Location is required');
|
||||
}
|
||||
if (typeof this.options.location !== 'string') this.error('Location can only be a string');
|
||||
if (this.options.location.startsWith('de:09162:')) return true;
|
||||
return false;
|
||||
},
|
||||
offset() {
|
||||
if (this.options.offset) return this.options.offset;
|
||||
return 0;
|
||||
},
|
||||
limit() {
|
||||
return this.options.limit || 10;
|
||||
},
|
||||
endpointDeparture() {
|
||||
return `${widgetApiEndpoints.mvg}/departure?globalId=${this.location}&limit=30&offsetInMinutes=${this.offset}&transportTypes=UBAHN,TRAM,BUS,SBAHN`;
|
||||
},
|
||||
endpointLocation() {
|
||||
return `${widgetApiEndpoints.mvg}/location?query=${encodeURIComponent(this.options.location)}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.fetchData();
|
||||
this.finishLoading();
|
||||
},
|
||||
fetchData() {
|
||||
if (this.location !== undefined) {
|
||||
this.makeRequest(this.endpointDeparture).then(
|
||||
(response) => { this.processData(response); },
|
||||
);
|
||||
}
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
let i = 0;
|
||||
const results = [];
|
||||
data
|
||||
.filter(this.filter_results)
|
||||
.sort(this.sort_results)
|
||||
.slice(0, this.limit).forEach((dep) => {
|
||||
results.push({ ...dep, key: `mvg-dep-${this.location}-${i}` });
|
||||
i += 1;
|
||||
});
|
||||
this.departures = results;
|
||||
},
|
||||
ensure_array(value) {
|
||||
if (typeof value === 'string') {
|
||||
return [value];
|
||||
}
|
||||
return value;
|
||||
},
|
||||
filter_results(value) {
|
||||
if (!this.options.filters) return true;
|
||||
let useEntry = (
|
||||
(!this.options.filters.line)
|
||||
|| this.ensure_array(this.options.filters.line).includes(value.label)
|
||||
);
|
||||
useEntry = useEntry
|
||||
&& (
|
||||
(!this.options.filters.product)
|
||||
|| this.ensure_array(this.options.filters.product)
|
||||
.some(x => x.toLowerCase() === value.transportType.toLowerCase())
|
||||
);
|
||||
useEntry = useEntry
|
||||
&& (
|
||||
(!this.options.filters.destination)
|
||||
|| this.ensure_array(this.options.filters.destination)
|
||||
.some(x => x.toLowerCase() === value.destination.toLowerCase())
|
||||
);
|
||||
return useEntry;
|
||||
},
|
||||
sort_results(a, b) {
|
||||
const depa = a.realtimeDepartureTime ? a.realtimeDepartureTime : a.plannedDepartureTime;
|
||||
const depb = b.realtimeDepartureTime ? b.realtimeDepartureTime : b.plannedDepartureTime;
|
||||
if (depa > depb) return 1;
|
||||
if (depa < depb) return -1;
|
||||
if (a.label < b.label) return 1;
|
||||
if (a.label > b.label) return -1;
|
||||
if (a.destination < b.destination) return 1;
|
||||
if (a.destination > b.destination) return -1;
|
||||
return 0;
|
||||
},
|
||||
makeUrl(cronId) {
|
||||
const base = this.options.host || 'https://healthchecks.io';
|
||||
return `${base}/checks/${cronId}/details`;
|
||||
},
|
||||
mvgTooltipDeparture(data) {
|
||||
let departureDetails = '';
|
||||
if (data.realtime) {
|
||||
departureDetails += `Live: ${timestampToTime(data.realtimeDepartureTime)}<br />`;
|
||||
}
|
||||
departureDetails += `Planned: ${timestampToTime(data.plannedDepartureTime)}<br />`;
|
||||
if (data.realtime) {
|
||||
departureDetails += 'Live!<br />';
|
||||
}
|
||||
return {
|
||||
content: departureDetails, html: true, trigger: 'hover', delay: 250, classes: 'mvg-info-tt',
|
||||
};
|
||||
},
|
||||
mvgTooltipDestination(data) {
|
||||
let departureDetails = `<b>Infos:</b><br />${data.messages.join('<br />')}`;
|
||||
if (data.platform) {
|
||||
departureDetails += `Platform: ${data.platform}<br />`;
|
||||
}
|
||||
if (data.cancelled) {
|
||||
departureDetails += '<b>Cancelled!</b><br />';
|
||||
}
|
||||
return {
|
||||
content: departureDetails, html: true, trigger: 'hover', delay: 250, classes: 'mvg-info-tt',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mvg-wrapper {
|
||||
display: grid;
|
||||
justify-content: left;
|
||||
grid-template-columns: 1fr 9fr;
|
||||
color: var(--widget-text-color);
|
||||
padding: 0.25rem 0;
|
||||
grid-row-gap: 0.4em;
|
||||
.departure {
|
||||
min-width: 1rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
margin-right: 0.2rem;
|
||||
span.live {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
.line {
|
||||
background-color: #FFFFFF;
|
||||
margin: 0;
|
||||
padding-right: 0.2em;
|
||||
border-radius: 0.2em;
|
||||
display: grid;
|
||||
grid-template-columns: 2.2em 1fr minmax(1.5em,max-content) 0.75em;
|
||||
.type-UBAHN {
|
||||
border: 0px;
|
||||
}
|
||||
.type-SBAHN {
|
||||
border: 0px;
|
||||
}
|
||||
.type-BUS {
|
||||
}
|
||||
.type-TRAM {
|
||||
}
|
||||
.transport{
|
||||
border-top-left-radius: 0.2em 0.2em;
|
||||
border-bottom-left-radius: 0.2em 0.2em;
|
||||
margin: 0em;
|
||||
padding: 0.15em 0;
|
||||
color: #FFFFFF;
|
||||
margin-right: 0.40em;
|
||||
text-align: center;
|
||||
span {
|
||||
min-width: 2em;
|
||||
display: inline-block;
|
||||
}
|
||||
&.line-U1 {
|
||||
background-color: #468447;
|
||||
}
|
||||
&.line-U2 {
|
||||
background-color: #dd3d4d;
|
||||
}
|
||||
&.line-U3 {
|
||||
background-color: #ef8824;
|
||||
}
|
||||
&.line-U4 {
|
||||
background-color: #04af90;
|
||||
}
|
||||
&.line-U5 {
|
||||
background-color: #b78730;
|
||||
}
|
||||
&.line-U6 {
|
||||
background-color: #0472b3;
|
||||
}
|
||||
&.line-S1 {
|
||||
background-color: #79c6e7;
|
||||
}
|
||||
&.line-S2 {
|
||||
background-color: #9bc04c;
|
||||
}
|
||||
&.line-S3 {
|
||||
background-color: #942d8d;
|
||||
}
|
||||
&.line-S4 {
|
||||
background-color: #d4214d;
|
||||
}
|
||||
&.line-S5 {
|
||||
background-color: #03a074;
|
||||
}
|
||||
&.line-S6 {
|
||||
background-color: #03a074;
|
||||
}
|
||||
&.line-S7 {
|
||||
background-color: #964438;
|
||||
}
|
||||
&.line-S8 {
|
||||
background-color: #000000;
|
||||
}
|
||||
&.type-BUS {
|
||||
background-color: #0d5c70;
|
||||
}
|
||||
}
|
||||
.destination{
|
||||
border-radius: 0.2em;
|
||||
width: 100%;
|
||||
background-color: #FFFFFF;
|
||||
color: #000;
|
||||
padding-top: 0.15em;
|
||||
padding-bottom: 0.15em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
span.cancelled {
|
||||
color: var(--danger);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
span.destination {
|
||||
overflow: clip;
|
||||
margin-right: 0.25em;
|
||||
width: 75%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.delay{
|
||||
padding: 0.15em;
|
||||
font-weight: bold;
|
||||
&.has-delay{
|
||||
padding: 0.15em;
|
||||
background-color: var(--danger);
|
||||
color: #FFF;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
}
|
||||
.delay::before{
|
||||
content: "+";
|
||||
}
|
||||
.occupancy{
|
||||
display: inline-block;
|
||||
padding: 0 0.15em;
|
||||
border-radius: 0.2em;
|
||||
&.occupancy-LOW {
|
||||
color: green;
|
||||
}
|
||||
&.occupancy-MEDIUM {
|
||||
color: orange;
|
||||
}
|
||||
&.occupancy-HIGH {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.ping-times-tt {
|
||||
min-width: 20rem;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.mvg-info-tt {
|
||||
min-width: 20rem;
|
||||
}
|
||||
</style>
|
||||
372
src/components/Widgets/MvgConnection.vue
Normal file
372
src/components/Widgets/MvgConnection.vue
Normal file
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<div class="mvg-connections-outer-wrapper">
|
||||
<div class="mvg-connections-header" v-if="showTitle">{{ connectionName }}</div>
|
||||
<div class="mvg-wrapper" v-if="connections">
|
||||
<div
|
||||
v-for="connection in connections"
|
||||
v-bind:key="connection.uniqueId"
|
||||
class="line"
|
||||
v-tooltip="mvgTooltipConnection(connection)"
|
||||
>
|
||||
<div
|
||||
class="departure"
|
||||
>
|
||||
<span class="time"
|
||||
>
|
||||
{{connection.parts[0].from.plannedDeparture | formatTime}}
|
||||
</span>
|
||||
<span class="delay"
|
||||
:class="{'has-delay': connection.parts[0].from.departureDelayInMinutes > 0}"
|
||||
>{{ Math.max(parseInt(connection.parts[0].from.departureDelayInMinutes) || 0, 0) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="changes"
|
||||
>
|
||||
<template
|
||||
v-for="(part,index) in connection.parts"
|
||||
>
|
||||
<span
|
||||
v-if="index > 0"
|
||||
v-bind:key="'change-' + index"
|
||||
class="change"
|
||||
v-tooltip="part.from.name"
|
||||
>⬌</span>
|
||||
<span
|
||||
v-bind:key="'transport-' + index"
|
||||
:class="['type-' + part.line.transportType,
|
||||
'line-' + part.line.label,
|
||||
]"
|
||||
v-if="part.line.transportType != 'PEDESTRIAN'"
|
||||
class="transport"
|
||||
>{{part.line.label}}</span>
|
||||
<span v-else
|
||||
v-bind:key="'transport-' + index"
|
||||
>🚶</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="time">
|
||||
{{Date.parse(connection.parts[connection.parts.length-1]
|
||||
.to.plannedDeparture) - Date.parse(connection.parts[0]
|
||||
.from.plannedDeparture) | formatDuration}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
connections: null,
|
||||
locationSearch: null,
|
||||
connectionName: null,
|
||||
defaultTitle: 'Connection',
|
||||
locations: {
|
||||
origin: undefined,
|
||||
destination: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
const promStart = this.getLocationId(this.start);
|
||||
const promEnd = this.getLocationId(this.end);
|
||||
Promise.all([promStart, promEnd]).then(
|
||||
(results) => {
|
||||
[this.locations.origin, this.locations.destination] = results.map((r) => r[0]);
|
||||
this.defaultTitle = `${this.locations.origin.name} - ${this.locations.destination.name}`;
|
||||
this.fetchData();
|
||||
},
|
||||
);
|
||||
},
|
||||
filters: {
|
||||
formatDepartureTime(timestamp) {
|
||||
const msDifference = new Date(timestamp).getTime() - new Date().getTime();
|
||||
const diff = Math.max(0, Math.round(msDifference / 60000));
|
||||
return diff;
|
||||
},
|
||||
formatTime(str) {
|
||||
const d = new Date(Date.parse(str));
|
||||
function ii(i) {
|
||||
let s = `${i}`;
|
||||
if (s.length < 2) s = `0${s}`;
|
||||
return s;
|
||||
}
|
||||
return `${ii(d.getHours())}:${ii(d.getMinutes())}`;
|
||||
},
|
||||
formatDuration(val) {
|
||||
function ii(i) {
|
||||
let s = `${i}`;
|
||||
if (s.length < 2) s = `0${s}`;
|
||||
return s;
|
||||
}
|
||||
return `${Math.floor(val / 3600000)}:${ii(Math.floor(val / 60000))}`;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
start() {
|
||||
return this.options.from || this.options.start || this.options.origin || 'Marienplatz';
|
||||
},
|
||||
end() {
|
||||
return this.options.to || this.options.end || this.options.destination || 'Giesing';
|
||||
},
|
||||
title() {
|
||||
if (this.options.title) {
|
||||
return this.options.title;
|
||||
}
|
||||
return this.defaultTitle;
|
||||
},
|
||||
showTitle() {
|
||||
return (this.options.header) ? this.options.header : true;
|
||||
},
|
||||
transportTypes() {
|
||||
if (this.options.transportations) {
|
||||
return this.options.transportations.join(',');
|
||||
}
|
||||
return 'UBAHN,TRAM,BUS,SBAHN';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatPoint(point, typ) {
|
||||
if (point.type === 'ADDRESS' || point.type === 'POI') {
|
||||
return `${typ}Latitude=${point.latitude}&${typ}Longitude=${point.longitude}`;
|
||||
}
|
||||
return `${typ}StationGlobalId=${point.globalId}`;
|
||||
},
|
||||
isLocationId(loc) {
|
||||
if (!loc) {
|
||||
this.error('Location is required');
|
||||
}
|
||||
if (typeof loc !== 'string') this.error('Location can only be a string');
|
||||
return (loc.startsWith('de:09162:'));
|
||||
},
|
||||
getLocationId(loc) {
|
||||
return this.makeRequest(this.getEndpointLocation(loc));
|
||||
},
|
||||
getEndpointLocation(loc) {
|
||||
return `${widgetApiEndpoints.mvg}/location?query=${encodeURIComponent(loc)}`;
|
||||
},
|
||||
endpointConnection() {
|
||||
return `${widgetApiEndpoints.mvg}/connection?${this.formatPoint(this.locations.origin, 'origin')}&${this.formatPoint(this.locations.destination, 'destination')}&routingDateTime=${(new Date()).toISOString()}&offsetInMinutes=${this.offset}&transportTypes=${this.transportTypes}`;
|
||||
},
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.fetchData();
|
||||
this.finishLoading();
|
||||
},
|
||||
fetchData() {
|
||||
if (this.locations.origin !== undefined
|
||||
&& this.locations.destination !== undefined) {
|
||||
this.makeRequest(this.endpointConnection()).then(
|
||||
(response) => { this.processData(response); },
|
||||
);
|
||||
}
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
this.connections = data;
|
||||
},
|
||||
ensure_array(value) {
|
||||
if (typeof value === 'string') {
|
||||
return [value];
|
||||
}
|
||||
return value;
|
||||
},
|
||||
mvgTooltipConnection(data) {
|
||||
let connectionDetails = '';
|
||||
const self = this;
|
||||
function addStep(step) {
|
||||
connectionDetails += `<b>${self.$options.filters.formatTime(step.plannedDeparture)}</b>
|
||||
<span class="delay">+${Math.max(parseInt(step.departureDelayInMinutes, 10) || 0, 0)}</span>
|
||||
<span>${step.name}</span>`;
|
||||
}
|
||||
addStep(data.parts[0].from);
|
||||
data.parts.forEach((part) => {
|
||||
addStep(part.to);
|
||||
});
|
||||
return {
|
||||
content: connectionDetails, html: true, trigger: 'hover', delay: 250, classes: 'mvg-connection-detail',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mvg-header {
|
||||
color: var(--widget-text-color);
|
||||
font-size:1.2em;
|
||||
}
|
||||
.mvg-wrapper {
|
||||
display: grid;
|
||||
justify-content: left;
|
||||
grid-template-columns: 100%;
|
||||
color: var(--widget-text-color);
|
||||
padding: 0.25rem 0;
|
||||
grid-row-gap: 0.4em;
|
||||
.departure {
|
||||
min-width: 1rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
margin-right: 0.2rem;
|
||||
span.live {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
.line {
|
||||
margin: 0em;
|
||||
padding: 0em;
|
||||
border-radius: 0.2em;
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 5fr 0.75fr;
|
||||
.changes {
|
||||
text-align: center;
|
||||
}
|
||||
.type-UBAHN {
|
||||
border: 0px;
|
||||
}
|
||||
.type-SBAHN {
|
||||
border: 0px;
|
||||
}
|
||||
.type-BUS {
|
||||
}
|
||||
.type-TRAM {
|
||||
background-color: #dd3d4d;
|
||||
}
|
||||
.transport{
|
||||
border-radius: 0.2em;
|
||||
margin: 0em;
|
||||
padding: 0.15em 0.15em;
|
||||
color: #FFFFFF;
|
||||
margin-right: 0.40em;
|
||||
margin-left: 0.40em;
|
||||
text-align: center;
|
||||
span {
|
||||
min-width: 2em;
|
||||
display: inline-block;
|
||||
}
|
||||
&.line-Fussweg {
|
||||
text-indent: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
&.line-U1 {
|
||||
background-color: #468447;
|
||||
}
|
||||
&.line-U2 {
|
||||
background-color: #dd3d4d;
|
||||
}
|
||||
&.line-U3 {
|
||||
background-color: #ef8824;
|
||||
}
|
||||
&.line-U4 {
|
||||
background-color: #04af90;
|
||||
}
|
||||
&.line-U5 {
|
||||
background-color: #b78730;
|
||||
}
|
||||
&.line-U6 {
|
||||
background-color: #0472b3;
|
||||
}
|
||||
&.line-S1 {
|
||||
background-color: #79c6e7;
|
||||
}
|
||||
&.line-S2 {
|
||||
background-color: #9bc04c;
|
||||
}
|
||||
&.line-S3 {
|
||||
background-color: #942d8d;
|
||||
}
|
||||
&.line-S4 {
|
||||
background-color: #d4214d;
|
||||
}
|
||||
&.line-S5 {
|
||||
background-color: #03a074;
|
||||
}
|
||||
&.line-S6 {
|
||||
background-color: #03a074;
|
||||
}
|
||||
&.line-S7 {
|
||||
background-color: #964438;
|
||||
}
|
||||
&.line-S8 {
|
||||
background-color: #000000;
|
||||
}
|
||||
&.type-BUS {
|
||||
background-color: #0d5c70;
|
||||
}
|
||||
}
|
||||
.destination{
|
||||
border-radius: 0.2em;
|
||||
width: 100%;
|
||||
background-color: #FFFFFF;
|
||||
color: #000;
|
||||
padding-top: 0.15em;
|
||||
padding-bottom: 0.15em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
span.cancelled {
|
||||
color: var(--danger);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
span.destination {
|
||||
overflow: clip;
|
||||
margin-right: 0.25em;
|
||||
width: 75%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.delay{
|
||||
padding: 0.15em;
|
||||
font-weight: bold;
|
||||
&.has-delay{
|
||||
padding: 0.15em;
|
||||
background-color: var(--danger);
|
||||
color: #FFF;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
}
|
||||
.delay::before{
|
||||
content: "+";
|
||||
}
|
||||
.occupancy{
|
||||
display: inline-block;
|
||||
padding: 0 0.15em;
|
||||
border-radius: 0.2em;
|
||||
&.occupancy-LOW {
|
||||
color: green;
|
||||
}
|
||||
&.occupancy-MEDIUM {
|
||||
color: orange;
|
||||
}
|
||||
&.occupancy-HIGH {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.ping-times-tt {
|
||||
min-width: 20rem;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.mvg-connection-detail .tooltip-inner {
|
||||
min-width: 20rem;
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 6fr;
|
||||
}
|
||||
</style>
|
||||
@@ -40,8 +40,12 @@ export default {
|
||||
if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server');
|
||||
return usersChoice || 'http://pi.hole';
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('API Key is required, please see the docs');
|
||||
return this.options.apiKey;
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/admin/api.php`;
|
||||
return `${this.hostname}/admin/api.php?summary&auth=${this.apiKey}`;
|
||||
},
|
||||
hideStatus() { return this.options.hideStatus; },
|
||||
hideChart() { return this.options.hideChart; },
|
||||
@@ -57,7 +61,11 @@ export default {
|
||||
fetchData() {
|
||||
this.makeRequest(this.endpoint)
|
||||
.then((response) => {
|
||||
this.processData(response);
|
||||
if (Array.isArray(response)) {
|
||||
this.error('Got success, but found no results, possible authorization error');
|
||||
} else {
|
||||
this.processData(response);
|
||||
}
|
||||
});
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
|
||||
@@ -23,8 +23,12 @@ export default {
|
||||
if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server');
|
||||
return usersChoice || 'http://pi.hole';
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) this.error('API Key is required, please see the docs');
|
||||
return this.options.apiKey;
|
||||
},
|
||||
endpoint() {
|
||||
return `${this.hostname}/admin/api.php?overTimeData10mins`;
|
||||
return `${this.hostname}/admin/api.php?overTimeData10mins&auth=${this.apiKey}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -38,7 +42,9 @@ export default {
|
||||
});
|
||||
},
|
||||
validate(response) {
|
||||
if (!response.ads_over_time || !response.domains_over_time) {
|
||||
if (Array.isArray(response)) {
|
||||
this.error('Got success, but found no results, possible authorization error');
|
||||
} else if (!response.ads_over_time || !response.domains_over_time) {
|
||||
this.error('Expected data was not returned from Pi-Hole');
|
||||
return false;
|
||||
} else if (response.ads_over_time.length < 1) {
|
||||
|
||||
156
src/components/Widgets/Proxmox.vue
Normal file
156
src/components/Widgets/Proxmox.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="proxmox-list">
|
||||
<div class="proxmox-title" v-if="title">
|
||||
<a v-if="titleAsLink" class="proxmox-link" :href="clusterUrl" target="_blank">
|
||||
{{ title }}
|
||||
</a>
|
||||
<span v-if="!titleAsLink">{{ title }}</span>
|
||||
</div>
|
||||
<div v-for="(item, key) in data" :key="key" class="proxmox-row">
|
||||
<div v-if="item.node" class="proxmox-cell">{{ item.node }}</div>
|
||||
<div v-if="item.name" class="proxmox-cell">{{ item.name }}</div>
|
||||
<div class="proxmox-cell proxmox-status"><span :class="item.status"></span></div>
|
||||
</div>
|
||||
<div class="proxmox-footer" v-if="footer">
|
||||
<a v-if="footerAsLink" class="proxmox-link" :href="clusterUrl" target="_blank">
|
||||
{{ footer }}
|
||||
</a>
|
||||
<span v-if="!footerAsLink">{{ title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
data: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
clusterUrl() {
|
||||
if (!this.options.cluster_url) this.error('The cluster URL is required.');
|
||||
return this.options.cluster_url || '';
|
||||
},
|
||||
userName() {
|
||||
if (!this.options.user_name) this.error('The user name is required.');
|
||||
return this.options.user_name || '';
|
||||
},
|
||||
tokenName() {
|
||||
if (!this.options.token_name) this.error('The token name is required.');
|
||||
return this.options.token_name || '';
|
||||
},
|
||||
tokenUuid() {
|
||||
if (!this.options.token_uuid) this.error('The token uuid is required.');
|
||||
return this.options.token_uuid || '';
|
||||
},
|
||||
node() {
|
||||
return this.options.node || '';
|
||||
},
|
||||
nodeData() {
|
||||
return this.options.node_data || false;
|
||||
},
|
||||
hideTemplates() {
|
||||
return this.options.hide_templates || false;
|
||||
},
|
||||
title() {
|
||||
return this.options.title || '';
|
||||
},
|
||||
titleAsLink() {
|
||||
return this.options.title_as_link || false;
|
||||
},
|
||||
footer() {
|
||||
return this.options.footer || '';
|
||||
},
|
||||
footerAsLink() {
|
||||
return this.options.footer_as_link || false;
|
||||
},
|
||||
endpoint() {
|
||||
if (!this.node) {
|
||||
return `${this.clusterUrl}/api2/json/nodes`;
|
||||
}
|
||||
if (this.nodeData) {
|
||||
return `${this.clusterUrl}/api2/json/nodes/${this.node}/${this.nodeData}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
authHeaders() {
|
||||
if (this.userName && this.tokenName && this.tokenUuid) {
|
||||
return { Authorization: `PVEAPIToken=${this.userName}!${this.tokenName}=${this.tokenUuid}` };
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
const auth = this.authHeaders;
|
||||
if (auth) {
|
||||
this.startLoading();
|
||||
this.makeRequest(this.endpoint, auth).then(this.processData);
|
||||
}
|
||||
},
|
||||
processData(data) {
|
||||
this.data = data.data.sort((a, b) => a.vmid > b.vmid);
|
||||
if (this.hideTemplates) {
|
||||
this.data = this.data.filter(item => item.template !== 1);
|
||||
}
|
||||
this.finishLoading();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.proxmox-list {
|
||||
.proxmox-title, .proxmox-footer {
|
||||
outline: 2px solid transparent;
|
||||
border: 1px solid var(--outline-color);
|
||||
border-radius: var(--curve-factor);
|
||||
box-shadow: var(--item-shadow);
|
||||
color: var(--item-text-color);
|
||||
margin: .5rem;
|
||||
padding: 0.3rem;
|
||||
background: var(--item-background);
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--item-text-color);
|
||||
}
|
||||
}
|
||||
.proxmox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--widget-text-color);
|
||||
font-size: 1.1rem;
|
||||
.proxmox-cell {
|
||||
display: inline-block;
|
||||
}
|
||||
.proxmox-status{
|
||||
.online, .running {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--success);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.proxmox-link {
|
||||
display: inline-block;
|
||||
padding: 0.2rem;
|
||||
margin: 0.1rem 0.2rem;
|
||||
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--widget-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -54,10 +54,17 @@ export default {
|
||||
const then = new Date((now.setMonth(now.getMonth() + this.monthsToShow)));
|
||||
return `${then.getDate()}-${then.getMonth() + 1}-${then.getFullYear()}`;
|
||||
},
|
||||
region() {
|
||||
if (this.options?.state) {
|
||||
return `®ion=${this.options.state}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
endpoint() {
|
||||
return `${widgetApiEndpoints.holidays}`
|
||||
+ `&fromDate=${this.startDate}&toDate=${this.endDate}`
|
||||
+ `&country=${this.country}&holidayType=${this.holidayType}`;
|
||||
+ `&country=${this.country}&holidayType=${this.holidayType}`
|
||||
+ `${this.region}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as Parser from 'rss-parser';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||
|
||||
@@ -47,11 +48,14 @@ export default {
|
||||
/* The URL to users atom-format RSS feed */
|
||||
rssUrl() {
|
||||
if (!this.options.rssUrl) this.error('Missing feed URL');
|
||||
return encodeURIComponent(this.options.rssUrl || '');
|
||||
return this.options.rssUrl || '';
|
||||
},
|
||||
apiKey() {
|
||||
return this.options.apiKey;
|
||||
},
|
||||
parseLocally() {
|
||||
return this.options.parseLocally;
|
||||
},
|
||||
limit() {
|
||||
const usersChoice = this.options.limit;
|
||||
if (usersChoice) return usersChoice;
|
||||
@@ -73,8 +77,12 @@ export default {
|
||||
const limit = this.limit && this.apiKey ? `&count=${this.limit}` : '';
|
||||
const orderBy = this.orderBy && this.apiKey ? `&order_by=${this.orderBy}` : '';
|
||||
const direction = this.orderDirection ? `&order_dir=${this.orderDirection}` : '';
|
||||
return `${widgetApiEndpoints.rssToJson}?rss_url=${this.rssUrl}`
|
||||
+ `${apiKey}${limit}${orderBy}${direction}`;
|
||||
if (this.parseLocally) {
|
||||
return this.rssUrl;
|
||||
} else {
|
||||
return `${widgetApiEndpoints.rssToJson}?rss_url=${encodeURIComponent(this.rssUrl)}`
|
||||
+ `${apiKey}${limit}${orderBy}${direction}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
@@ -88,31 +96,53 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Make GET request to Rss2Json */
|
||||
/* Make GET request to whatever endpoint we are using */
|
||||
fetchData() {
|
||||
this.makeRequest(this.endpoint).then(this.processData);
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
const { feed, items } = data;
|
||||
this.meta = {
|
||||
title: feed.title,
|
||||
link: feed.link,
|
||||
author: feed.author,
|
||||
description: feed.description,
|
||||
image: feed.image,
|
||||
};
|
||||
async processData(data) {
|
||||
if (this.parseLocally) {
|
||||
const parser = new Parser();
|
||||
const {
|
||||
link, title, items, author, description, image,
|
||||
} = await parser.parseString(data);
|
||||
this.meta = {
|
||||
title,
|
||||
link,
|
||||
author,
|
||||
description,
|
||||
image,
|
||||
};
|
||||
this.processItems(items);
|
||||
} else {
|
||||
const { feed, items } = data;
|
||||
this.meta = {
|
||||
title: feed.title,
|
||||
link: feed.link,
|
||||
author: feed.author,
|
||||
description: feed.description,
|
||||
image: feed.image,
|
||||
};
|
||||
this.processItems(items);
|
||||
}
|
||||
},
|
||||
processItems(items) {
|
||||
const posts = [];
|
||||
items.forEach((post) => {
|
||||
let { length } = items;
|
||||
if (this.limit) {
|
||||
length = this.limit;
|
||||
}
|
||||
for (let i = 0; length > i; i += 1) {
|
||||
posts.push({
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
image: post.thumbnail,
|
||||
author: post.author,
|
||||
date: post.pubDate,
|
||||
link: post.link,
|
||||
title: items[i].title,
|
||||
description: items[i].description,
|
||||
image: items[i].thumbnail,
|
||||
author: items[i].author,
|
||||
date: items[i].pubDate,
|
||||
link: items[i].link,
|
||||
});
|
||||
});
|
||||
}
|
||||
this.posts = posts;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -52,10 +52,12 @@ const COMPAT = {
|
||||
clock: 'Clock',
|
||||
'crypto-price-chart': 'CryptoPriceChart',
|
||||
'crypto-watch-list': 'CryptoWatchList',
|
||||
'custom-search': 'CustomSearch',
|
||||
'cve-vulnerabilities': 'CveVulnerabilities',
|
||||
'domain-monitor': 'DomainMonitor',
|
||||
'code-stats': 'CodeStats',
|
||||
'covid-stats': 'CovidStats',
|
||||
'drone-ci': 'DroneCi',
|
||||
embed: 'EmbedWidget',
|
||||
'eth-gas-prices': 'EthGasPrices',
|
||||
'exchange-rates': 'ExchangeRates',
|
||||
@@ -82,6 +84,9 @@ const COMPAT = {
|
||||
image: 'ImageWidget',
|
||||
joke: 'Jokes',
|
||||
'mullvad-status': 'MullvadStatus',
|
||||
mvg: 'Mvg',
|
||||
linkding: 'Linkding',
|
||||
'mvg-connection': 'MvgConnection',
|
||||
'nd-cpu-history': 'NdCpuHistory',
|
||||
'nd-load-history': 'NdLoadHistory',
|
||||
'nd-ram-history': 'NdRamHistory',
|
||||
@@ -95,6 +100,7 @@ const COMPAT = {
|
||||
'pi-hole-stats': 'PiHoleStats',
|
||||
'pi-hole-top-queries': 'PiHoleTopQueries',
|
||||
'pi-hole-traffic': 'PiHoleTraffic',
|
||||
'proxmox-lists': 'Proxmox',
|
||||
'public-holidays': 'PublicHolidays',
|
||||
'public-ip': 'PublicIp',
|
||||
'rss-feed': 'RssFeed',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="xkcd-wrapper">
|
||||
<div class="xkcd-wrapper" v-tooltip="toolTip(alt)">
|
||||
<h3 class="xkcd-title">{{ title }}</h3>
|
||||
<a :href="`https://xkcd.com/${comicNum}/`">
|
||||
<img :src="image" :alt="alt" class="xkcd-comic" />
|
||||
<img :src="image" :alt="alt" class="xkcd-comic"/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,6 +59,12 @@ export default {
|
||||
this.alt = data.alt;
|
||||
this.comicNum = data.num;
|
||||
},
|
||||
toolTip(alt) {
|
||||
const content = alt;
|
||||
return {
|
||||
content, html: false, trigger: 'hover focus', delay: 250, classes: 'xkcd-alt-tt',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -80,3 +86,8 @@ export default {
|
||||
}
|
||||
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.xkcd-alt-tt {
|
||||
min-width: 20rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
</div>
|
||||
<!-- Show links for switching back to Home / Minimal views -->
|
||||
<div class="switch-view-buttons">
|
||||
<router-link to="/home">
|
||||
<router-link to="/home/">
|
||||
<IconHome class="view-icon" v-tooltip="$t('alternate-views.default')" />
|
||||
</router-link>
|
||||
<router-link to="/minimal">
|
||||
<router-link to="/minimal/">
|
||||
<IconMinimalView class="view-icon" v-tooltip="$t('alternate-views.minimal')" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="web-content" :id="id">
|
||||
<iframe :src="url" allow="fullscreen" />
|
||||
<iframe :src="url" allow="fullscreen; clipboard-write" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user