Merge branch 'Lissy93:master' into BUG/1608_glances-network-error
This commit is contained in:
@@ -34,6 +34,8 @@
|
||||
:statusSuccess="statusResponse ? statusResponse.successStatus : undefined"
|
||||
:statusText="statusResponse ? statusResponse.message : undefined"
|
||||
/>
|
||||
<!-- URL of the item (shown on hover, only on some themes) -->
|
||||
<p class="item-url">{{ item.url | shortUrl }}</p>
|
||||
<!-- Edit icon (displayed only when in edit mode) -->
|
||||
<EditModeIcon v-if="isEditMode" class="edit-mode-item" @click="openItemSettings()" />
|
||||
</a>
|
||||
@@ -122,6 +124,26 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
shortUrl(value) {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
// Use URL constructor to parse the input
|
||||
const url = new URL(value);
|
||||
return url.hostname;
|
||||
} catch (e) {
|
||||
// If the input is not a valid URL, try to handle it as an IP address
|
||||
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}/;
|
||||
const match = value.match(ipPattern);
|
||||
if (match) {
|
||||
return match[0];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editMenuOpen: false,
|
||||
@@ -209,6 +231,9 @@ export default {
|
||||
&.span-7 { min-width: 14%; }
|
||||
&.span-8 { min-width: 12.5%; }
|
||||
}
|
||||
.item-url {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
|
||||
@@ -24,6 +24,8 @@ export default {
|
||||
endpoint() {
|
||||
if (this.provider === 'ipgeolocation') {
|
||||
return `${widgetApiEndpoints.publicIp2}?apiKey=${this.apiKey}`;
|
||||
} else if (this.provider === 'ip2location.io') {
|
||||
return `${widgetApiEndpoints.publicIp4}?key=${this.apiKey}`;
|
||||
} else if (this.provider === 'ipapi') {
|
||||
return widgetApiEndpoints.publicIp3;
|
||||
}
|
||||
@@ -31,10 +33,11 @@ export default {
|
||||
},
|
||||
apiKey() {
|
||||
if (this.provider === 'ipgeolocation' && !this.options.apiKey) this.error('Missing API Key');
|
||||
if (this.provider === 'ip2location.io' && !this.options.apiKey) this.error('Missing API Key');
|
||||
return this.options.apiKey;
|
||||
},
|
||||
provider() {
|
||||
// Can be either `ip-api`, `ipapi.co` or `ipgeolocation`
|
||||
// Can be either `ip-api`, `ipapi.co`, `ipgeolocation` or `ip2location.io`
|
||||
return this.parseAsEnvVar(this.options.provider) || 'ipapi.co';
|
||||
},
|
||||
},
|
||||
@@ -72,6 +75,12 @@ export default {
|
||||
this.location = `${ipInfo.city}, ${ipInfo.regionName}`;
|
||||
this.flagImg = getCountryFlag(ipInfo.countryCode);
|
||||
this.mapsUrl = getMapUrl({ lat: ipInfo.lat, lon: ipInfo.lon });
|
||||
} else if (this.provider === 'ip2location.io') {
|
||||
this.ipAddr = ipInfo.ip;
|
||||
this.ispName = ipInfo.isp || 'IP2Location.io Starter plan or higher required.';
|
||||
this.location = `${ipInfo.city_name}, ${ipInfo.region_name}`;
|
||||
this.flagImg = getCountryFlag(ipInfo.country_code);
|
||||
this.mapsUrl = getMapUrl({ lat: ipInfo.latitude, lon: ipInfo.longitude });
|
||||
} else {
|
||||
this.error('Unknown API provider fo IP address');
|
||||
}
|
||||
|
||||
227
src/components/Widgets/TacticalRMM.vue
Normal file
227
src/components/Widgets/TacticalRMM.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div class="status-section">
|
||||
<template v-if="statusData">
|
||||
<div class="status-wrapper">
|
||||
<div class="status-item">
|
||||
<div class="title">Version</div>
|
||||
<div class="value">{{ statusData.version }}</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="title">Agent Count</div>
|
||||
<div class="value">{{ statusData.agent_count }}</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="title">Client Count</div>
|
||||
<div class="value">{{ statusData.client_count }}</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="title">Site Count</div>
|
||||
<div class="value">{{ statusData.site_count }}</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="title">Disk Usage</div>
|
||||
<div class="value">{{ statusData.disk_usage_percent }}%</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="title">Memory Usage</div>
|
||||
<div class="value">{{ statusData.mem_usage_percent }}%</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="title">Days Until Cert Expires</div>
|
||||
<div class="value">{{ statusData.days_until_cert_expires }}</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="title">Cert Expired</div>
|
||||
<div class="value">{{ statusData.cert_expired ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
<div class="status-item services">
|
||||
<div class="title">Services Running</div>
|
||||
<div class="services-list">
|
||||
<div
|
||||
v-for="(status, service) in statusData.services_running"
|
||||
:key="service"
|
||||
class="service"
|
||||
>
|
||||
<span class="service-name">{{ service }}</span>
|
||||
<span :class="['service-status', status ? 'running' : 'stopped']">
|
||||
{{ status ? 'Running' : 'Stopped' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="errorMessage">
|
||||
<div class="error-message">
|
||||
<span class="text">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { serviceEndpoints } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statusData: null,
|
||||
errorMessage: null,
|
||||
errorMessageConstants: {
|
||||
missingToken: 'No Token set',
|
||||
missingUrl: 'No URL set',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
},
|
||||
computed: {
|
||||
token() {
|
||||
return this.parseAsEnvVar(this.options.token);
|
||||
},
|
||||
url() {
|
||||
return this.parseAsEnvVar(this.options.url);
|
||||
},
|
||||
authHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
},
|
||||
proxyReqEndpoint() {
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
return `${baseUrl}${serviceEndpoints.corsProxy}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.startLoading();
|
||||
this.fetchData();
|
||||
},
|
||||
fetchData() {
|
||||
const {
|
||||
authHeaders, url, token, proxyReqEndpoint,
|
||||
} = this;
|
||||
|
||||
if (!this.optionsValid({ url, token })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetURL = url;
|
||||
const customHeaders = JSON.stringify(authHeaders);
|
||||
|
||||
axios.post(
|
||||
proxyReqEndpoint,
|
||||
{ auth: token },
|
||||
{
|
||||
headers: {
|
||||
'Target-URL': targetURL,
|
||||
CustomHeaders: customHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((response) => {
|
||||
this.processData(response.data);
|
||||
})
|
||||
.catch(() => {
|
||||
this.errorMessage = 'Failed to fetch data';
|
||||
})
|
||||
.finally(() => {
|
||||
this.finishLoading();
|
||||
});
|
||||
},
|
||||
processData(response) {
|
||||
this.statusData = response;
|
||||
},
|
||||
optionsValid({ url, token }) {
|
||||
const errors = [];
|
||||
if (!url) {
|
||||
errors.push(this.errorMessageConstants.missingUrl);
|
||||
}
|
||||
if (!token) {
|
||||
errors.push(this.errorMessageConstants.missingToken);
|
||||
}
|
||||
if (errors.length === 0) {
|
||||
return true;
|
||||
}
|
||||
this.errorMessage = errors.join('\n');
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.status-section {
|
||||
background-color: var(--item-background);
|
||||
padding: 1em;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.status-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
color: var(--item-text-color);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
width: 48%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
color: var(--item-text-color);
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-top: 0.5em;
|
||||
color: var(--item-text-color);
|
||||
}
|
||||
|
||||
.services-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.service {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0.5em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-weight: bold;
|
||||
color: var(--item-text-color);
|
||||
}
|
||||
|
||||
.service-status {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.service-status.running {
|
||||
color: var(--success);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.service-status.stopped {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--item-text-color);
|
||||
}
|
||||
</style>
|
||||
@@ -46,7 +46,7 @@ const COMPAT = {
|
||||
'adguard-filter-status': 'AdGuardFilterStatus',
|
||||
'adguard-stats': 'AdGuardStats',
|
||||
'adguard-top-domains': 'AdGuardTopDomains',
|
||||
anonaddy: 'AnonAddy',
|
||||
anonaddy: 'addy.io',
|
||||
apod: 'Apod',
|
||||
'blacklist-check': 'BlacklistCheck',
|
||||
clock: 'Clock',
|
||||
@@ -115,6 +115,7 @@ const COMPAT = {
|
||||
'synology-download': 'SynologyDownload',
|
||||
'system-info': 'SystemInfo',
|
||||
'tfl-status': 'TflStatus',
|
||||
trmm: 'TacticalRMM',
|
||||
'uptime-kuma': 'UptimeKuma',
|
||||
'wallet-balance': 'WalletBalance',
|
||||
weather: 'Weather',
|
||||
|
||||
@@ -1849,6 +1849,88 @@ html[data-theme='neomorphic'] {
|
||||
}
|
||||
|
||||
|
||||
html[data-theme="night-bat"] {
|
||||
// Main colors
|
||||
--primary: #4780ff;
|
||||
--background: #252931;
|
||||
--background-darker: #303540;
|
||||
// Typography
|
||||
--font-headings: 'Podkova', 'Roboto', serif;
|
||||
--font-body: 'Roboto', serif;
|
||||
--heading-text-color: #fff;
|
||||
// Items
|
||||
--item-background: #303540;
|
||||
--item-background-hover: var(--item-background);
|
||||
--item-shadow: 0px 3px 0px var(--primary), 2px 2px 6px var(--black);
|
||||
--item-hover-shadow: 0px 20px 0px 0 var(--primary), 2px 2px 6px var(--black);
|
||||
// Sections
|
||||
--item-group-heading-text-color: var(--white);
|
||||
--item-group-heading-text-color-hover: var(--white);
|
||||
--item-group-shadow: none;
|
||||
--item-group-background: none;
|
||||
--item-group-outer-background: none;
|
||||
// Nav Links
|
||||
--nav-link-background-color: var(--background);
|
||||
--nav-link-background-color-hover: var(--background);
|
||||
--nav-link-border-color: transparent;
|
||||
--nav-link-border-color-hover: transparent;
|
||||
--nav-link-shadow: 4px 4px 0px var(--background-darker), -3px 0px 0px var(--primary), 2px 2px 6px var(--black);
|
||||
--nav-link-shadow-hover: 6px 6px 0px var(--background-darker), -4px 0px 0px var(--primary), 2px 2px 9px var(--black);
|
||||
// Misc
|
||||
--curve-factor: 4px;
|
||||
--curve-factor-navbar: 8px;
|
||||
|
||||
--widget-text-color: var(--white);
|
||||
|
||||
// Style overrides
|
||||
label.lbl-toggle h3 { font-size: 1.3rem; font-weight: bold; }
|
||||
.content-inner { border-top: 1px dashed var(--primary); }
|
||||
.item.size-large .tile-title p.description { height: 3rem; }
|
||||
.item, .nav-outer nav .nav-item { border-radius: 1px; }
|
||||
.item.size-large { margin: 0.5rem; }
|
||||
// Show outline when collapsed
|
||||
.is-collapsed {
|
||||
background: var(--item-background);
|
||||
box-shadow: var(--item-shadow);
|
||||
&:hover {
|
||||
background: var(--item-background-hover);
|
||||
box-shadow: var(--item-hover-shadow);
|
||||
}
|
||||
}
|
||||
.widget-base {
|
||||
background: var(--background-darker);
|
||||
padding: 1rem 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.item-wrapper {
|
||||
.item-url {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: -1.9rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--background);
|
||||
transition: all 0.2s cubic-bezier(0.8, 0.8, 0.4, 1.4);
|
||||
}
|
||||
a {
|
||||
transition: all 0.2s cubic-bezier(0.8, 0.8, 0.4, 1.4);
|
||||
height: calc(100% - 1rem);
|
||||
}
|
||||
&:hover {
|
||||
a { height: calc(100% - 2rem); }
|
||||
.item-icon {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
.item-url {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
html[data-theme='cherry-blossom'] {
|
||||
--primary: #e1e8ee;
|
||||
--background: #11171d;
|
||||
@@ -1999,7 +2081,7 @@ html[data-theme="tama"] {
|
||||
// Background Image
|
||||
body {
|
||||
//update the query terms after the '?', to customize for images you want
|
||||
background: url('https://source.unsplash.com/random/1920x1080/?dark,calm,nature,background');
|
||||
background: url('https://picsum.photos/1920/1080');
|
||||
background-color: var(--background-darker);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
@@ -565,7 +565,12 @@
|
||||
"title": "OIDC Client Id",
|
||||
"type": "string",
|
||||
"description": "ClientId from OIDC provider"
|
||||
}
|
||||
},
|
||||
"scope" : {
|
||||
"title": "OIDC Scope",
|
||||
"type": "string",
|
||||
"description": "The scope(s) to request from the OIDC provider"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableHeaderAuth": {
|
||||
|
||||
@@ -13,14 +13,14 @@ const getAppConfig = () => {
|
||||
class OidcAuth {
|
||||
constructor() {
|
||||
const { auth } = getAppConfig();
|
||||
const { clientId, endpoint } = auth.oidc;
|
||||
const { clientId, endpoint, scope } = auth.oidc;
|
||||
const settings = {
|
||||
userStore: new WebStorageStateStore({ store: window.localStorage }),
|
||||
authority: endpoint,
|
||||
client_id: clientId,
|
||||
redirect_uri: `${window.location.origin}`,
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email roles groups',
|
||||
scope: scope || 'openid profile email roles groups',
|
||||
response_mode: 'query',
|
||||
filterProtocolClaims: true,
|
||||
};
|
||||
|
||||
@@ -89,6 +89,7 @@ module.exports = {
|
||||
'tama',
|
||||
'neomorphic',
|
||||
'glass-2',
|
||||
'night-bat',
|
||||
],
|
||||
/* Default color options for the theme configurator swatches */
|
||||
swatches: [
|
||||
@@ -220,7 +221,7 @@ module.exports = {
|
||||
},
|
||||
/* API endpoints for widgets that need to fetch external data */
|
||||
widgetApiEndpoints: {
|
||||
anonAddy: 'https://app.anonaddy.com',
|
||||
anonAddy: 'https://app.addy.io',
|
||||
astronomyPictureOfTheDay: 'https://apod.as93.net/apod',
|
||||
blacklistCheck: 'https://api.blacklistchecker.com/check',
|
||||
codeStats: 'https://codestats.net/',
|
||||
@@ -244,6 +245,7 @@ module.exports = {
|
||||
publicIp: 'https://ipapi.co/json',
|
||||
publicIp2: 'https://api.ipgeolocation.io/ipgeo',
|
||||
publicIp3: 'http://ip-api.com/json',
|
||||
publicIp4: 'https://api.ip2location.io/',
|
||||
readMeStats: 'https://github-readme-stats.vercel.app/api',
|
||||
rescueTime: 'https://www.rescuetime.com/anapi/data',
|
||||
rssToJson: 'https://api.rss2json.com/v1/api.json',
|
||||
|
||||
Reference in New Issue
Block a user