mirror of
https://github.com/smartyellow/status.git
synced 2025-07-19 20:58:02 +00:00
268
gui/components/formautotestfield.svelte
Normal file
268
gui/components/formautotestfield.svelte
Normal file
@ -0,0 +1,268 @@
|
||||
<script>
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Toggle from 'components/webdesq/toggle.svelte';
|
||||
import { operatorNames } from '../../lib/operators';
|
||||
import { realValueNames } from '../../lib/realvalues';
|
||||
|
||||
export let value = [];
|
||||
export let specs = {};
|
||||
export let readonly = true;
|
||||
export let language = 'en';
|
||||
export let translate = s => s;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const defaultEndpoint = {
|
||||
uri: '',
|
||||
headers: [],
|
||||
requirements: [],
|
||||
};
|
||||
const defaultReq = {
|
||||
type: 'httpstatus',
|
||||
truth: 'true',
|
||||
operator: 'equal',
|
||||
string: ''
|
||||
};
|
||||
const defaultHeader = {
|
||||
name: '',
|
||||
value: '',
|
||||
};
|
||||
|
||||
const changeValue = () => dispatch('changeValue', value) && console.log(value);;
|
||||
const appendEndpoint = () => value = [...value, defaultEndpoint];
|
||||
|
||||
function removeEndpoint(i) {
|
||||
value.splice(i, 1);
|
||||
value = value;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if specs.label}
|
||||
<span class="label" class:alignright={specs.labelPosition === 'right'}>
|
||||
{translate(specs.label, language)}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#each value as endpoint, iEndpoint (endpoint)}
|
||||
<div class="endpoint">
|
||||
<div>
|
||||
<label for="uri-{iEndpoint}">
|
||||
{translate('Endpoint URI', language)}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<input
|
||||
id="uri-{iEndpoint}"
|
||||
type="text"
|
||||
placeholder="https://"
|
||||
disabled={readonly}
|
||||
bind:value={endpoint.uri}
|
||||
on:focus
|
||||
on:blur={changeValue}
|
||||
/>
|
||||
<button on:click={() => removeEndpoint(iEndpoint)}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if endpoint.headers?.length > 0}
|
||||
<strong>{translate('Headers', language)}</strong>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{translate('Header name', language)}</th>
|
||||
<th>{translate('Value', language)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#each endpoint.headers as header, iHeader (header)}
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={translate('name...', language)}
|
||||
disabled={readonly}
|
||||
bind:value={header.name}
|
||||
on:focus
|
||||
on:blur={changeValue}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={translate('value...', language)}
|
||||
disabled={readonly}
|
||||
bind:value={header.value}
|
||||
on:focus
|
||||
on:blur={changeValue}
|
||||
/>
|
||||
<button on:click={() => {
|
||||
endpoint.headers.splice(iHeader, 1);
|
||||
endpoint = endpoint;
|
||||
}}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<button on:click={() => endpoint.headers = [...endpoint.headers, defaultHeader]}>
|
||||
{translate('add request header', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if endpoint.requirements?.length > 0}
|
||||
<strong>{translate('Requirements', language)}</strong>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{translate('Real value', language)}</th>
|
||||
<th class="center">{translate('Truth', language)}</th>
|
||||
<th>{translate('Operator', language)}</th>
|
||||
<th>{translate('Value', language)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#each endpoint.requirements as req, iReq (req)}
|
||||
<tr>
|
||||
<td>
|
||||
<select
|
||||
bind:value={req.type}
|
||||
disabled={readonly}
|
||||
on:focus
|
||||
on:keyup={changeValue}
|
||||
on:change={changeValue}
|
||||
on:blur={changeValue}
|
||||
>
|
||||
{#each Object.keys(realValueNames) as valName}
|
||||
<option value={valName}>{realValueNames[valName]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<td class="center">
|
||||
<Toggle
|
||||
inline
|
||||
labels={{
|
||||
on: translate('is', language),
|
||||
off: translate('isn\'t', language)
|
||||
}}
|
||||
bind:value={req.truth}
|
||||
on:change={changeValue}
|
||||
{readonly}
|
||||
{language}
|
||||
{translate}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<select
|
||||
bind:value={req.operator}
|
||||
disabled={readonly}
|
||||
on:focus
|
||||
on:keyup={changeValue}
|
||||
on:change={changeValue}
|
||||
on:blur={changeValue}
|
||||
>
|
||||
{#each Object.keys(operatorNames) as opName}
|
||||
<option value={opName}>{operatorNames[opName]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={translate('value...', language)}
|
||||
disabled={readonly}
|
||||
bind:value={req.string}
|
||||
on:focus
|
||||
on:blur={changeValue}
|
||||
/>
|
||||
<button on:click={() => {
|
||||
endpoint.requirements.splice(iReq, 1);
|
||||
endpoint = endpoint;
|
||||
}}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<button on:click={() => endpoint.requirements = [...endpoint.requirements, defaultReq]}>
|
||||
{translate('add requirement', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div>
|
||||
<button on:click={appendEndpoint}>
|
||||
{translate('add endpoint', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
label, strong {
|
||||
font-weight: 700;
|
||||
line-height: 1em;
|
||||
margin-bottom: .7em;
|
||||
display: block;
|
||||
}
|
||||
label:not(:first-child), strong:not(:first-child) {
|
||||
margin-top: 1em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid rgba(0, 0, 0, .2);
|
||||
}
|
||||
input, select {
|
||||
padding: 0.4em;
|
||||
}
|
||||
.endpoint {
|
||||
border: 1px solid rgba(226, 226, 226, .76);
|
||||
background-color: rgba(70, 90, 131, .07);
|
||||
border-radius: 3px;
|
||||
padding: .5rem;
|
||||
}
|
||||
table {
|
||||
margin-left: -2px;
|
||||
width: calc(100% + 2px);
|
||||
}
|
||||
thead tr th {
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
}
|
||||
.flex input {
|
||||
flex: 1 0;
|
||||
}
|
||||
.flex button {
|
||||
min-width: 35px;
|
||||
flex: 0 1;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
152
gui/components/formoutagetablefield.svelte
Normal file
152
gui/components/formoutagetablefield.svelte
Normal file
@ -0,0 +1,152 @@
|
||||
<script>
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { api } from 'helpers/webdesq/stores.js';
|
||||
|
||||
export let value = ''; // contains webservice id
|
||||
export let specs = {};
|
||||
export let language = 'en';
|
||||
export let translate = s => s;
|
||||
|
||||
const icons = {
|
||||
server: '<path d="M0 308.58v150.83a57.73 57.73 0 0 0 10.69 33.38H757.3a57.62 57.62 0 0 0 10.7-33.37V308.58a57.73 57.73 0 0 0-10.69-33.38H10.7A57.76 57.76 0 0 0 0 308.58Zm665.6 81.82a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM640 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 0-25.6ZM588.8 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM537.6 352a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM512 390.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM486.4 352a12.8 12.8 0 1 1-.01 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM435.2 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-300.8-25.6A57.67 57.67 0 0 1 192 384a57.67 57.67 0 0 1-57.6 57.6A57.67 57.67 0 0 1 76.8 384a57.67 57.67 0 0 1 57.6-57.6Zm622.91-76.8A57.76 57.76 0 0 0 768 216.22V65.38A59.05 59.05 0 0 0 709.02 6.4H58.98A59.05 59.05 0 0 0 0 65.38V216.2a57.73 57.73 0 0 0 10.69 33.39H757.3Zm-91.7-102.4a12.8 12.8 0 1 1-.02 25.6 12.8 12.8 0 0 1 .01-25.6ZM640 108.8a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM512 147.2a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1-.01 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6ZM134.4 83.2a57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6 57.67 57.67 0 0 1-57.6-57.6 57.67 57.67 0 0 1 57.6-57.6ZM10.69 518.4A57.76 57.76 0 0 0 0 551.78v150.83c0 32.53 26.46 59 58.98 59H709a59.05 59.05 0 0 0 58.99-59V551.79a57.73 57.73 0 0 0-10.69-33.38Zm123.7 166.4a57.67 57.67 0 0 1-57.59-57.6 57.67 57.67 0 0 1 57.6-57.6 57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6Zm300.8-64a12.8 12.8 0 1 1 .02-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.61 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 0-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.6-38.4a12.8 12.8 0 1 1 0-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 .01-25.61 12.8 12.8 0 0 1 0 25.6Zm0 0"/>',
|
||||
};
|
||||
const severity = {
|
||||
major: {
|
||||
name: 'major',
|
||||
class: 'l1',
|
||||
},
|
||||
minor: {
|
||||
name: 'minor',
|
||||
class: 'l2',
|
||||
},
|
||||
scheduled: {
|
||||
name: 'scheduled',
|
||||
class: 'l3',
|
||||
},
|
||||
none: {
|
||||
name: 'no impact',
|
||||
class: 'l4',
|
||||
},
|
||||
}
|
||||
const dispatch = createEventDispatcher();
|
||||
const service = value; // for clarity
|
||||
let outages = [];
|
||||
|
||||
function openOutage(title, id) {
|
||||
dispatch('openitem', {
|
||||
type: 'smartyellow/webserviceoutages',
|
||||
title: title,
|
||||
icon: icons.server,
|
||||
closeable: true,
|
||||
isNew: false,
|
||||
data: { id: id },
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
outages = (await api.get('/outages')).filter(o => o.services?.includes(service));
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if specs.label}
|
||||
<span class="label" class:alignright={specs.labelPosition === 'right'}>
|
||||
{translate(specs.label, language)}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{translate('id', language)}</th>
|
||||
<th>{translate('name', language)}</th>
|
||||
<th>{translate('severity', language)}</th>
|
||||
<th class="center"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#each outages as outage}
|
||||
{@const name = outage.name[language] || outage.name.en}
|
||||
<tr>
|
||||
<td>{outage.id}</td>
|
||||
<td>{name}</td>
|
||||
<td>
|
||||
{#if severity[outage.severity]}
|
||||
<span class="state {severity[outage.severity]?.class}">
|
||||
{severity[outage.severity]?.name}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="state l0">
|
||||
{translate('unclassified', language)}
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="center">
|
||||
<button class="small" on:click={() => openOutage(name, outage.id)}>
|
||||
{translate('open', language)}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.table-container {
|
||||
overflow: auto;
|
||||
}
|
||||
.table {
|
||||
table-layout: fixed;
|
||||
min-width: 100%;
|
||||
border-spacing: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
th {
|
||||
padding: 3px 6px;
|
||||
border: 1px solid #ccc;
|
||||
border-width: 0 1px 1px 0;
|
||||
background: #eee;
|
||||
font-weight: normal;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
}
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
td {
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
border-width: 0 1px 1px 0;
|
||||
padding: 3px 6px;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
tr > td:first-child {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
span.state.l0 {
|
||||
background-color: #808080;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
button.small {
|
||||
padding: 3px 10px;
|
||||
min-width: 20px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
176
gui/modules/webservicemonitor.svelte
Normal file
176
gui/modules/webservicemonitor.svelte
Normal file
@ -0,0 +1,176 @@
|
||||
<script>
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { translate, api } from 'helpers/webdesq/stores.js';
|
||||
|
||||
import PanelManager from 'components/webdesq/panelmanager.svelte';
|
||||
import Panel from 'components/webdesq/panel.svelte';
|
||||
|
||||
export let language = 'en';
|
||||
|
||||
const icons = {
|
||||
wrench: '<path d="M175 631c0-15-13-28-29-28s-29 13-29 28 13 27 29 27 29-12 29-27zm294-180L158 743a61 61 0 0 1-41 16c-16 0-31-6-42-16l-48-46a52 52 0 0 1 0-78l310-292c24 57 72 102 132 124zm289-187c0 15-6 32-11 46a206 206 0 0 1-192 129c-113 0-205-86-205-192S442 55 555 55c33 0 76 9 104 27 5 3 7 7 7 12s-3 9-7 12l-134 72v96l88 46c15-8 121-71 130-71s15 7 15 15zm0 0"/>',
|
||||
server: '<path d="M0 308.58v150.83a57.73 57.73 0 0 0 10.69 33.38H757.3a57.62 57.62 0 0 0 10.7-33.37V308.58a57.73 57.73 0 0 0-10.69-33.38H10.7A57.76 57.76 0 0 0 0 308.58Zm665.6 81.82a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM640 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 0-25.6ZM588.8 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM537.6 352a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM512 390.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM486.4 352a12.8 12.8 0 1 1-.01 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM435.2 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-300.8-25.6A57.67 57.67 0 0 1 192 384a57.67 57.67 0 0 1-57.6 57.6A57.67 57.67 0 0 1 76.8 384a57.67 57.67 0 0 1 57.6-57.6Zm622.91-76.8A57.76 57.76 0 0 0 768 216.22V65.38A59.05 59.05 0 0 0 709.02 6.4H58.98A59.05 59.05 0 0 0 0 65.38V216.2a57.73 57.73 0 0 0 10.69 33.39H757.3Zm-91.7-102.4a12.8 12.8 0 1 1-.02 25.6 12.8 12.8 0 0 1 .01-25.6ZM640 108.8a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM512 147.2a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1-.01 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6ZM134.4 83.2a57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6 57.67 57.67 0 0 1-57.6-57.6 57.67 57.67 0 0 1 57.6-57.6ZM10.69 518.4A57.76 57.76 0 0 0 0 551.78v150.83c0 32.53 26.46 59 58.98 59H709a59.05 59.05 0 0 0 58.99-59V551.79a57.73 57.73 0 0 0-10.69-33.38Zm123.7 166.4a57.67 57.67 0 0 1-57.59-57.6 57.67 57.67 0 0 1 57.6-57.6 57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6Zm300.8-64a12.8 12.8 0 1 1 .02-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.61 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 0-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.6-38.4a12.8 12.8 0 1 1 0-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 .01-25.61 12.8 12.8 0 0 1 0 25.6Zm0 0"/>',
|
||||
};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let mounted = false;
|
||||
let error = false;
|
||||
let webservices = [];
|
||||
|
||||
async function refresh() {
|
||||
console.log('refresh');
|
||||
try {
|
||||
webservices = await api.get('/webservices');
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
function openWebService(title, id) {
|
||||
dispatch('openitem', {
|
||||
type: 'smartyellow/webservices',
|
||||
title: title,
|
||||
icon: icons.server,
|
||||
closeable: true,
|
||||
isNew: false,
|
||||
data: { id: id },
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await refresh();
|
||||
const interval = setInterval(async () => await refresh(), 10_000);
|
||||
|
||||
mounted = true;
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<div class="error">
|
||||
{translate('Failed to fetch fresh data', language)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if mounted}
|
||||
|
||||
<PanelManager>
|
||||
<Panel>
|
||||
|
||||
{#if webservices.length}
|
||||
<div class="servicelist">
|
||||
{#each webservices as service}
|
||||
{@const name = service.name[language] || service.name.en}
|
||||
<div class="service">
|
||||
<div class="title">{name}</div>
|
||||
|
||||
<div class="date">
|
||||
{@html translate('Status last checked on: <m>', [ `<span>${new Date(service.lastChecked).toLocaleString()}</span>`, language ])}
|
||||
</div>
|
||||
|
||||
<div class="tags">
|
||||
{#if service.heartbeat[service.heartbeat.length - 1]?.down == true}
|
||||
<span class="tag red">DOWN</span>
|
||||
{:else}
|
||||
<span class="tag green">UP</span>
|
||||
{/if}
|
||||
|
||||
<span class="tag light link" on:click={() => openWebService(name, service.id)}>
|
||||
open
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
No webservices
|
||||
{/if}
|
||||
|
||||
</Panel>
|
||||
</PanelManager>
|
||||
|
||||
{:else}
|
||||
<h2>Loading...</h2>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
div:not(:last-child) {
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.servicelist {
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 3px;
|
||||
}
|
||||
.servicelist .service {
|
||||
background-color: #fff;
|
||||
margin-bottom: 1px;
|
||||
padding: 1em;
|
||||
}
|
||||
.servicelist .service .title {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: #808080;
|
||||
color: #fff;
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.tag.green {
|
||||
background-color: #007000;
|
||||
}
|
||||
.tag.red {
|
||||
background-color: #980000;
|
||||
}
|
||||
.tag.light {
|
||||
background-color: rgba(0, 0, 0, .1);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tag.link {
|
||||
text-decoration: underline;
|
||||
transition: background-color .2s;
|
||||
}
|
||||
.tag.link.light:hover {
|
||||
background-color: rgba(0, 0, 0, .2);
|
||||
}
|
||||
.tag.link::after {
|
||||
content: '';
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
margin-left: .3em;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: rgba(0, 0, 0, .5);
|
||||
}
|
||||
.date :global(span) {
|
||||
font-style: italic;
|
||||
opacity: 1.3;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.servicelist {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
276
gui/modules/webserviceoutages.svelte
Normal file
276
gui/modules/webserviceoutages.svelte
Normal file
@ -0,0 +1,276 @@
|
||||
<script>
|
||||
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { user, users, prefix, translate, api } from 'helpers/webdesq/stores.js';
|
||||
import shareQuery from 'helpers/webdesq/sharequery.js';
|
||||
|
||||
// import regular components
|
||||
import PanelManager from 'components/webdesq/panelmanager.svelte';
|
||||
import Panel from 'components/webdesq/panel.svelte';
|
||||
import Toolbar from 'components/webdesq/toolbar.svelte';
|
||||
import ToolbarButton from 'components/webdesq/toolbarbutton.svelte';
|
||||
import SaveOrDiscard from 'components/webdesq/saveordiscard.svelte';
|
||||
import Multifilter from 'components/webdesq/multifilter.svelte';
|
||||
import Grid from 'components/webdesq/grid.svelte';
|
||||
import Form from 'components/webdesq/form.svelte';
|
||||
import ObjectTree from 'components/webdesq/objecttree.svelte';
|
||||
import EditHistory from 'components/webdesq/edithistory.svelte';
|
||||
import CompareVersions from 'components/webdesq/compareversions.svelte';
|
||||
|
||||
// props
|
||||
export let focused = false;
|
||||
export let language = 'en';
|
||||
export let icon = false;
|
||||
export let id = false;
|
||||
export let modified = false;
|
||||
export let notifications = false;
|
||||
|
||||
// local state
|
||||
const dispatch = createEventDispatcher();
|
||||
const confirm = getContext('confirm');
|
||||
const entity = 'smartyellow/webserviceoutage';
|
||||
const moduleName = 'smartyellow/webserviceoutages';
|
||||
const pluginName = 'smartyellow/status';
|
||||
const readonly = user.cannot(pluginName + '/editOutages');
|
||||
|
||||
const icons = {
|
||||
trash: '<path d="M638 77H501V25c0-14-11-25-25-25H292c-14 0-25 11-25 25v52H130c-14 0-25 11-25 25v77c0 14 11 25 25 25h508c14 0 25-11 25-25v-77c0-14-11-25-25-25zm-187 0H317V50h134zm0 0M140 254l21 490c0 13 11 24 25 24h396c14 0 25-11 25-24l21-490zm168 412a25 25 0 01-50 0V356a25 25 0 0150 0zm101 0a25 25 0 11-50 0V356a25 25 0 0150 0zm101 0a25 25 0 01-50 0V356a25 25 0 0150 0zm0 0"/>',
|
||||
history: '<path d="M402 238v183l157 92 26-44-128-76V238zm0 0"/><path d="M439 55a329 329 0 00-329 329H0l142 142 3 6 148-148H183a256 256 0 1175 181l-52 52A329 329 0 10438 55zm0 0"/>',
|
||||
wrench: '<path d="M175 631c0-15-13-28-29-28s-29 13-29 28 13 27 29 27 29-12 29-27zm294-180L158 743a61 61 0 0 1-41 16c-16 0-31-6-42-16l-48-46a52 52 0 0 1 0-78l310-292c24 57 72 102 132 124zm289-187c0 15-6 32-11 46a206 206 0 0 1-192 129c-113 0-205-86-205-192S442 55 555 55c33 0 76 9 104 27 5 3 7 7 7 12s-3 9-7 12l-134 72v96l88 46c15-8 121-71 130-71s15 7 15 15zm0 0"/>',
|
||||
lexicon: '<path d="M685 313H396c-46 0-83 37-83 83v157l-75 54a22 22 0 000 36l76 54c6 40 41 71 82 71h289c46 0 83-37 83-83V396c0-46-37-83-83-83zm-97 316c-5 0-9-1-11-6l-9-32h-55l-9 32c-1 5-5 6-11 6-8 0-19-5-19-13v-2l47-152c2-7 11-10 20-10s18 3 20 10l46 152 1 2c0 8-12 13-20 13zm0 0"/><path d="M520 567h42l-21-74zm0 0M268 396c0-30 11-58 28-80-25 0-49-8-69-22-19 14-43 22-69 22a13 13 0 010-25c18 0 35-5 49-14-17-19-28-43-31-70h-18a13 13 0 110-25h57v-31a13 13 0 1125 0v31h56a13 13 0 110 25h-17c-3 27-14 51-32 70 15 9 32 14 49 14 7 0 13 5 13 12 23-22 54-35 87-35h59v-53l75-54a22 22 0 000-36l-76-54c-6-40-41-71-82-71H83C37 0 0 37 0 83v289c0 46 37 83 83 83h185zm0 0"/><path d="M227 261c14-14 24-33 26-54h-52c3 21 12 40 26 54zm0 0"/>',
|
||||
preview: '<path d="m45 631 158-135c-35-49-55-110-55-174 0-167 133-301 295-301 164 0 296 134 296 301 0 166-132 301-296 301-63 0-122-21-170-56L140 728l-9 8a66 66 0 0 1-94-8 69 69 0 0 1 8-97Zm623-309c0-127-101-229-225-229a227 227 0 0 0-224 229c0 126 100 229 224 229s225-104 225-229Zm0 0"/><path d="M281 294c33-30 94-75 162-75 69 0 130 44 163 75 17 15 17 41 0 56-33 30-94 75-163 75-68 0-129-44-162-75a39 39 0 0 1 0-56Zm162 94c37 0 65-30 65-66s-29-66-65-66c-35 0-65 30-65 66s29 66 65 66Zm0 0"/><path d="M409 322c0 19 16 34 34 34 19 0 34-15 34-34s-15-35-34-35c-18 0-34 16-34 35" />',
|
||||
};
|
||||
|
||||
const gridOptions = {
|
||||
icons,
|
||||
index: true,
|
||||
storageKey: moduleName,
|
||||
multiselect: true,
|
||||
header: true,
|
||||
footer: false,
|
||||
rowselect: true,
|
||||
};
|
||||
|
||||
let mounted = false;
|
||||
let settings = {};
|
||||
let item, backup = false;
|
||||
let isNew = id === true;
|
||||
let dragdata = false;
|
||||
let form;
|
||||
let log;
|
||||
let filters = {};
|
||||
let items = [];
|
||||
let errors = {};
|
||||
let grid;
|
||||
let savebar;
|
||||
let multifilter;
|
||||
let debug = false;
|
||||
let history = false;
|
||||
let selectedVersions = false;
|
||||
let preview = false;
|
||||
|
||||
|
||||
onMount(async function () {
|
||||
try {
|
||||
if (id) {
|
||||
// get item, form and log for specified id
|
||||
try {
|
||||
// get settings, to read previewUrl
|
||||
settings = await api.get('/outages/settings');
|
||||
({ item, form, log } = isNew ? await api.post('/outages', {}, { init: true }) : await api.get('/outages/' + id));
|
||||
// if existing item, set tabtitle to title of item
|
||||
if (!isNew) {
|
||||
dispatch('tabchanged', { title: item.name });
|
||||
}
|
||||
api.subscribe(pluginName + '/reload', async msg => {
|
||||
if (msg.id == item.id) {
|
||||
({ item, form, log } = await api.get('/outages' + id));
|
||||
}
|
||||
});
|
||||
api.subscribe(entity + '/reload', async () => {
|
||||
({ form, log } = await api.get('/outages/' + id));
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
console.log('could not open item', id);
|
||||
dispatch('unpin', { id: id, module: moduleName });
|
||||
dispatch('close');
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
filters = await api.get('/outages/filters');
|
||||
gridOptions.columns = await api.get('/outages/formats');
|
||||
// subscribe to 'reload' message
|
||||
api.subscribe(pluginName + '/reload', async () => {
|
||||
multifilter.submit();
|
||||
});
|
||||
}
|
||||
mounted = true;
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Failed to fetch outages', e);
|
||||
}
|
||||
});
|
||||
|
||||
function openItem(event, newItem = false) {
|
||||
dispatch('openitem', {
|
||||
type: moduleName,
|
||||
title: newItem ? 'new outage' : '',
|
||||
icon: icon,
|
||||
closeable: true,
|
||||
isNew: newItem,
|
||||
data: {
|
||||
id: event.detail.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
if (focused) {
|
||||
// save changes for outage in form
|
||||
savebar.start();
|
||||
const result = isNew ?
|
||||
await api.post('/outages', item) :
|
||||
await api.put('/outages/' + item.id, item);
|
||||
savebar.stop(result);
|
||||
if (!result.errors) {
|
||||
log = result.log;
|
||||
// Once saved, we need to use PUT request to update newly created outages
|
||||
isNew = false;
|
||||
// Update tab title
|
||||
dispatch('tabchanged', { title: item.title });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submitFilters({ detail }) {
|
||||
if (grid) {
|
||||
grid.reset();
|
||||
}
|
||||
items = await api.post('/outages/search', detail);
|
||||
}
|
||||
|
||||
async function deleteItem() {
|
||||
if (item && item.id) {
|
||||
try {
|
||||
await confirm({
|
||||
msg: translate('Are you sure you want to delete this item?', language),
|
||||
});
|
||||
dispatch('close');
|
||||
await api.delete('/outages/' + item.id);
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function previewItem() {
|
||||
if (preview) {
|
||||
preview = false;
|
||||
}
|
||||
else {
|
||||
let p = settings.previewUrl;
|
||||
p = p.replace(':slug', (item.slug[language] ? item.slug[language] : item.slug.nl));
|
||||
preview = {
|
||||
url: p,
|
||||
};
|
||||
console.log('outgoing preview', preview);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{#if mounted}
|
||||
|
||||
{#if item}
|
||||
|
||||
<Toolbar large="768" fill="true">
|
||||
{#if !readonly}
|
||||
<SaveOrDiscard {language} {translate} {notifications} bind:errors bind:modified bind:item bind:backup bind:this={savebar} on:save={saveChanges} />
|
||||
{/if}
|
||||
{#if log && log.created && settings && settings.previewUrl}
|
||||
<ToolbarButton title="{translate('preview', language)}" icon={icons.preview} hint="{translate('preview', language)}" submenu>
|
||||
<li class:checked="{preview}" on:click="{previewItem}">Page preview</li>
|
||||
<li on:click="{() => window.open((settings.previewUrl.split('/')[3]))}">Website preview</li>
|
||||
</ToolbarButton>
|
||||
{/if}
|
||||
{#if form && form.languages}
|
||||
<ToolbarButton icon={icons.lexicon} title="{translate('language', language)}" hint="{translate('Language', language)}" submenu>
|
||||
{#each form.languages as l}
|
||||
<li class:checked={l.enabled} on:click={() => (l.enabled = !l.enabled)}>
|
||||
{l.name}
|
||||
</li>
|
||||
{/each}
|
||||
</ToolbarButton>
|
||||
{/if}
|
||||
{#if log && log.created}
|
||||
<ToolbarButton icon={icons.history} bind:value={history} title="{translate('history', language)}" hint="{translate('History', language)}" />
|
||||
{/if}
|
||||
{#if log && log.created && user.can(pluginName + '/deleteServices')}
|
||||
<ToolbarButton icon={icons.trash} on:click={deleteItem} title="{translate('delete', language)}" hint="{translate('Delete', language)}" />
|
||||
{/if}
|
||||
{#if user.is('sysadmin')}
|
||||
<ToolbarButton icon={icons.wrench} bind:value={debug} title="{translate('debug', language)}" hint="{translate('Show debugger', language)}" />
|
||||
{/if}
|
||||
</Toolbar>
|
||||
|
||||
<PanelManager>
|
||||
<Panel>
|
||||
<Form bind:data={item} prefix={$prefix} {api} {form} {readonly} {modified} {language} {translate} {errors} {debug} {notifications} on:save={saveChanges} />
|
||||
</Panel>
|
||||
{#if preview}
|
||||
<Panel width="50%">
|
||||
<iframe title="preview" src="{preview.url}"></iframe>
|
||||
</Panel>
|
||||
{/if}
|
||||
{#if history}
|
||||
{#if selectedVersions}
|
||||
<Panel width="40%">
|
||||
<CompareVersions {language} {translate} versions={selectedVersions} {form} />
|
||||
</Panel>
|
||||
{/if}
|
||||
<Panel width="25%">
|
||||
<EditHistory bind:backup bind:item bind:errors bind:selectedVersions user={$user} {api} {entity} {log} {readonly} {language} {translate} />
|
||||
</Panel>
|
||||
{/if}
|
||||
{#if debug}
|
||||
<Panel width="30%">
|
||||
<ObjectTree bind:data={item} />
|
||||
</Panel>
|
||||
{/if}
|
||||
</PanelManager>
|
||||
|
||||
{:else}
|
||||
|
||||
<Toolbar large="768">
|
||||
<Multifilter
|
||||
{moduleName}
|
||||
{language}
|
||||
{translate}
|
||||
{dragdata}
|
||||
users={Object.keys($users)}
|
||||
options={filters}
|
||||
placeholder={translate('Fill in criteria', language)}
|
||||
on:submit={submitFilters}
|
||||
on:share={e => shareQuery(e, api)}
|
||||
bind:this={multifilter}
|
||||
/>
|
||||
{#if user.can(pluginName + '/createServices')}
|
||||
<ToolbarButton {icon} on:click={e => openItem(e, true)} title="{translate('new', language)}" hint="{translate('Create new outage', language)}" />
|
||||
{/if}
|
||||
</Toolbar>
|
||||
<Grid bind:this={grid} {language} {items} {translate} options={gridOptions} bind:dragdata on:select={openItem} />
|
||||
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.panel>iframe) {
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
276
gui/modules/webservices.svelte
Normal file
276
gui/modules/webservices.svelte
Normal file
@ -0,0 +1,276 @@
|
||||
<script>
|
||||
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { user, users, prefix, translate, api } from 'helpers/webdesq/stores.js';
|
||||
import shareQuery from 'helpers/webdesq/sharequery.js';
|
||||
|
||||
// import regular components
|
||||
import PanelManager from 'components/webdesq/panelmanager.svelte';
|
||||
import Panel from 'components/webdesq/panel.svelte';
|
||||
import Toolbar from 'components/webdesq/toolbar.svelte';
|
||||
import ToolbarButton from 'components/webdesq/toolbarbutton.svelte';
|
||||
import SaveOrDiscard from 'components/webdesq/saveordiscard.svelte';
|
||||
import Multifilter from 'components/webdesq/multifilter.svelte';
|
||||
import Grid from 'components/webdesq/grid.svelte';
|
||||
import Form from 'components/webdesq/form.svelte';
|
||||
import ObjectTree from 'components/webdesq/objecttree.svelte';
|
||||
import EditHistory from 'components/webdesq/edithistory.svelte';
|
||||
import CompareVersions from 'components/webdesq/compareversions.svelte';
|
||||
|
||||
// props
|
||||
export let focused = false;
|
||||
export let language = 'en';
|
||||
export let icon = false;
|
||||
export let id = false;
|
||||
export let modified = false;
|
||||
export let notifications = false;
|
||||
|
||||
// local state
|
||||
const dispatch = createEventDispatcher();
|
||||
const confirm = getContext('confirm');
|
||||
const entity = 'smartyellow/webservice';
|
||||
const moduleName = 'smartyellow/webservices';
|
||||
const pluginName = 'smartyellow/status';
|
||||
const readonly = user.cannot(pluginName + '/editServices');
|
||||
|
||||
const icons = {
|
||||
trash: '<path d="M638 77H501V25c0-14-11-25-25-25H292c-14 0-25 11-25 25v52H130c-14 0-25 11-25 25v77c0 14 11 25 25 25h508c14 0 25-11 25-25v-77c0-14-11-25-25-25zm-187 0H317V50h134zm0 0M140 254l21 490c0 13 11 24 25 24h396c14 0 25-11 25-24l21-490zm168 412a25 25 0 01-50 0V356a25 25 0 0150 0zm101 0a25 25 0 11-50 0V356a25 25 0 0150 0zm101 0a25 25 0 01-50 0V356a25 25 0 0150 0zm0 0"/>',
|
||||
history: '<path d="M402 238v183l157 92 26-44-128-76V238zm0 0"/><path d="M439 55a329 329 0 00-329 329H0l142 142 3 6 148-148H183a256 256 0 1175 181l-52 52A329 329 0 10438 55zm0 0"/>',
|
||||
wrench: '<path d="M175 631c0-15-13-28-29-28s-29 13-29 28 13 27 29 27 29-12 29-27zm294-180L158 743a61 61 0 0 1-41 16c-16 0-31-6-42-16l-48-46a52 52 0 0 1 0-78l310-292c24 57 72 102 132 124zm289-187c0 15-6 32-11 46a206 206 0 0 1-192 129c-113 0-205-86-205-192S442 55 555 55c33 0 76 9 104 27 5 3 7 7 7 12s-3 9-7 12l-134 72v96l88 46c15-8 121-71 130-71s15 7 15 15zm0 0"/>',
|
||||
lexicon: '<path d="M685 313H396c-46 0-83 37-83 83v157l-75 54a22 22 0 000 36l76 54c6 40 41 71 82 71h289c46 0 83-37 83-83V396c0-46-37-83-83-83zm-97 316c-5 0-9-1-11-6l-9-32h-55l-9 32c-1 5-5 6-11 6-8 0-19-5-19-13v-2l47-152c2-7 11-10 20-10s18 3 20 10l46 152 1 2c0 8-12 13-20 13zm0 0"/><path d="M520 567h42l-21-74zm0 0M268 396c0-30 11-58 28-80-25 0-49-8-69-22-19 14-43 22-69 22a13 13 0 010-25c18 0 35-5 49-14-17-19-28-43-31-70h-18a13 13 0 110-25h57v-31a13 13 0 1125 0v31h56a13 13 0 110 25h-17c-3 27-14 51-32 70 15 9 32 14 49 14 7 0 13 5 13 12 23-22 54-35 87-35h59v-53l75-54a22 22 0 000-36l-76-54c-6-40-41-71-82-71H83C37 0 0 37 0 83v289c0 46 37 83 83 83h185zm0 0"/><path d="M227 261c14-14 24-33 26-54h-52c3 21 12 40 26 54zm0 0"/>',
|
||||
preview: '<path d="m45 631 158-135c-35-49-55-110-55-174 0-167 133-301 295-301 164 0 296 134 296 301 0 166-132 301-296 301-63 0-122-21-170-56L140 728l-9 8a66 66 0 0 1-94-8 69 69 0 0 1 8-97Zm623-309c0-127-101-229-225-229a227 227 0 0 0-224 229c0 126 100 229 224 229s225-104 225-229Zm0 0"/><path d="M281 294c33-30 94-75 162-75 69 0 130 44 163 75 17 15 17 41 0 56-33 30-94 75-163 75-68 0-129-44-162-75a39 39 0 0 1 0-56Zm162 94c37 0 65-30 65-66s-29-66-65-66c-35 0-65 30-65 66s29 66 65 66Zm0 0"/><path d="M409 322c0 19 16 34 34 34 19 0 34-15 34-34s-15-35-34-35c-18 0-34 16-34 35" />',
|
||||
};
|
||||
|
||||
const gridOptions = {
|
||||
icons,
|
||||
index: true,
|
||||
storageKey: moduleName,
|
||||
multiselect: true,
|
||||
header: true,
|
||||
footer: false,
|
||||
rowselect: true,
|
||||
};
|
||||
|
||||
let mounted = false;
|
||||
let settings = {};
|
||||
let item, backup = false;
|
||||
let isNew = id === true;
|
||||
let dragdata = false;
|
||||
let form;
|
||||
let log;
|
||||
let filters = {};
|
||||
let items = [];
|
||||
let errors = {};
|
||||
let grid;
|
||||
let savebar;
|
||||
let multifilter;
|
||||
let debug = false;
|
||||
let history = false;
|
||||
let selectedVersions = false;
|
||||
let preview = false;
|
||||
|
||||
|
||||
onMount(async function () {
|
||||
try {
|
||||
if (id) {
|
||||
// get item, form and log for specified id
|
||||
try {
|
||||
// get settings, to read previewUrl
|
||||
settings = await api.get('/webservices/settings');
|
||||
({ item, form, log } = isNew ? await api.post('/webservices', {}, { init: true }) : await api.get('/webservices/' + id));
|
||||
// if existing item, set tabtitle to title of item
|
||||
if (!isNew) {
|
||||
dispatch('tabchanged', { title: item.name });
|
||||
}
|
||||
api.subscribe(pluginName + '/reload', async msg => {
|
||||
if (msg.id == item.id) {
|
||||
({ item, form, log } = await api.get('/webservices' + id));
|
||||
}
|
||||
});
|
||||
api.subscribe(entity + '/reload', async () => {
|
||||
({ form, log } = await api.get('/webservices/' + id));
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
console.log('could not open item', id);
|
||||
dispatch('unpin', { id: id, module: moduleName });
|
||||
dispatch('close');
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
filters = await api.get('/webservices/filters');
|
||||
gridOptions.columns = await api.get('/webservices/formats');
|
||||
// subscribe to 'reload' message
|
||||
api.subscribe(pluginName + '/reload', async () => {
|
||||
multifilter.submit();
|
||||
});
|
||||
}
|
||||
mounted = true;
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Failed to fetch web services', e);
|
||||
}
|
||||
});
|
||||
|
||||
function openItem(event, newItem = false) {
|
||||
dispatch('openitem', {
|
||||
type: moduleName,
|
||||
title: newItem ? 'new web service' : '',
|
||||
icon: icon,
|
||||
closeable: true,
|
||||
isNew: newItem,
|
||||
data: {
|
||||
id: event.detail.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
if (focused) {
|
||||
// save changes for web service in form
|
||||
savebar.start();
|
||||
const result = isNew ?
|
||||
await api.post('/webservices', item) :
|
||||
await api.put('/webservices/' + item.id, item);
|
||||
savebar.stop(result);
|
||||
if (!result.errors) {
|
||||
log = result.log;
|
||||
// Once saved, we need to use PUT request to update newly created web service
|
||||
isNew = false;
|
||||
// Update tab title
|
||||
dispatch('tabchanged', { title: item.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submitFilters({ detail }) {
|
||||
if (grid) {
|
||||
grid.reset();
|
||||
}
|
||||
items = await api.post('/webservices/search', detail);
|
||||
}
|
||||
|
||||
async function deleteItem() {
|
||||
if (item && item.id) {
|
||||
try {
|
||||
await confirm({
|
||||
msg: translate('Are you sure you want to delete this item?', language),
|
||||
});
|
||||
dispatch('close');
|
||||
await api.delete('/webservices/' + item.id);
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function previewItem() {
|
||||
if (preview) {
|
||||
preview = false;
|
||||
}
|
||||
else {
|
||||
let p = settings.previewUrl;
|
||||
p = p.replace(':slug', (item.slug[language] ? item.slug[language] : item.slug.nl));
|
||||
preview = {
|
||||
url: p,
|
||||
};
|
||||
console.log('outgoing preview', preview);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{#if mounted}
|
||||
|
||||
{#if item}
|
||||
|
||||
<Toolbar large="768" fill="true">
|
||||
{#if !readonly}
|
||||
<SaveOrDiscard {language} {translate} {notifications} bind:errors bind:modified bind:item bind:backup bind:this={savebar} on:save={saveChanges} />
|
||||
{/if}
|
||||
{#if log && log.created && settings && settings.previewUrl}
|
||||
<ToolbarButton title="{translate('preview', language)}" icon={icons.preview} hint="{translate('preview', language)}" submenu>
|
||||
<li class:checked="{preview}" on:click="{previewItem}">Page preview</li>
|
||||
<li on:click="{() => window.open((settings.previewUrl.split('/')[3]))}">Website preview</li>
|
||||
</ToolbarButton>
|
||||
{/if}
|
||||
{#if form && form.languages}
|
||||
<ToolbarButton icon={icons.lexicon} title="{translate('language', language)}" hint="{translate('Language', language)}" submenu>
|
||||
{#each form.languages as l}
|
||||
<li class:checked={l.enabled} on:click={() => (l.enabled = !l.enabled)}>
|
||||
{l.name}
|
||||
</li>
|
||||
{/each}
|
||||
</ToolbarButton>
|
||||
{/if}
|
||||
{#if log && log.created}
|
||||
<ToolbarButton icon={icons.history} bind:value={history} title="{translate('history', language)}" hint="{translate('History', language)}" />
|
||||
{/if}
|
||||
{#if log && log.created && user.can(pluginName + '/deleteServices')}
|
||||
<ToolbarButton icon={icons.trash} on:click={deleteItem} title="{translate('delete', language)}" hint="{translate('Delete', language)}" />
|
||||
{/if}
|
||||
{#if user.is('sysadmin')}
|
||||
<ToolbarButton icon={icons.wrench} bind:value={debug} title="{translate('debug', language)}" hint="{translate('Show debugger', language)}" />
|
||||
{/if}
|
||||
</Toolbar>
|
||||
|
||||
<PanelManager>
|
||||
<Panel>
|
||||
<Form bind:data={item} prefix={$prefix} {api} {form} {readonly} {modified} {language} {translate} {errors} {debug} {notifications} on:save={saveChanges} />
|
||||
</Panel>
|
||||
{#if preview}
|
||||
<Panel width="50%">
|
||||
<iframe title="preview" src="{preview.url}"></iframe>
|
||||
</Panel>
|
||||
{/if}
|
||||
{#if history}
|
||||
{#if selectedVersions}
|
||||
<Panel width="40%">
|
||||
<CompareVersions {language} {translate} versions={selectedVersions} {form} />
|
||||
</Panel>
|
||||
{/if}
|
||||
<Panel width="25%">
|
||||
<EditHistory bind:backup bind:item bind:errors bind:selectedVersions user={$user} {api} {entity} {log} {readonly} {language} {translate} />
|
||||
</Panel>
|
||||
{/if}
|
||||
{#if debug}
|
||||
<Panel width="30%">
|
||||
<ObjectTree bind:data={item} />
|
||||
</Panel>
|
||||
{/if}
|
||||
</PanelManager>
|
||||
|
||||
{:else}
|
||||
|
||||
<Toolbar large="768">
|
||||
<Multifilter
|
||||
{moduleName}
|
||||
{language}
|
||||
{translate}
|
||||
{dragdata}
|
||||
users={Object.keys($users)}
|
||||
options={filters}
|
||||
placeholder={translate('Fill in criteria', language)}
|
||||
on:submit={submitFilters}
|
||||
on:share={e => shareQuery(e, api)}
|
||||
bind:this={multifilter}
|
||||
/>
|
||||
{#if user.can(pluginName + '/createServices')}
|
||||
<ToolbarButton {icon} on:click={e => openItem(e, true)} title="{translate('new', language)}" hint="{translate('Create new web service', language)}" />
|
||||
{/if}
|
||||
</Toolbar>
|
||||
<Grid bind:this={grid} {language} {items} {translate} options={gridOptions} bind:dragdata on:select={openItem} />
|
||||
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.panel>iframe) {
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
129
gui/widgets/webservicestatus.svelte
Normal file
129
gui/widgets/webservicestatus.svelte
Normal file
@ -0,0 +1,129 @@
|
||||
<script>
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { translate, api } from 'helpers/webdesq/stores.js';
|
||||
|
||||
export let language = 'en';
|
||||
export const settings = {
|
||||
title: {
|
||||
type: 'string',
|
||||
label: translate('title', language),
|
||||
},
|
||||
};
|
||||
|
||||
const icons = {
|
||||
warning: '<path d="M384 0C172.27 0 0 172.25 0 384s172.27 384 384 384 384-172.25 384-384S595.73 0 384 0Zm29.54 605.54a29.55 29.55 0 0 1-59.08 0V576a29.55 29.55 0 0 1 59.08 0Zm0-118.16a29.55 29.55 0 0 1-59.08 0V162.46a29.55 29.55 0 0 1 59.08 0Zm0 0"/>',
|
||||
check: '<path d="M655.65 112.36c-149.8-149.8-393.5-149.8-543.3 0-149.8 149.76-149.8 393.53 0 543.3C187.23 730.57 285.62 768 384 768c98.38 0 196.73-37.43 271.65-112.34 149.8-149.77 149.8-393.54 0-543.3Zm-56.92 166.22-224.1 224.1a31.93 31.93 0 0 1-22.65 9.39c-8.2 0-16.39-3.14-22.63-9.39L201.29 374.62a31.98 31.98 0 0 1 0-45.26 31.98 31.98 0 0 1 45.27 0l105.42 105.42 201.48-201.47a31.98 31.98 0 0 1 45.27 0 31.98 31.98 0 0 1 0 45.27Zm0 0"/>',
|
||||
}
|
||||
let promise;
|
||||
|
||||
async function refresh() {
|
||||
promise = await api.get('/webservices');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
refresh();
|
||||
const interval = setInterval(() => refresh(), 10_000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#await promise}
|
||||
{translate('Loading...', language)}
|
||||
{:then data}
|
||||
{@const servicesDown = data?.filter(d => d.heartbeat[d.heartbeat.length - 1]?.down == true)}
|
||||
{@const servicesUp = data?.filter(d => d.heartbeat[d.heartbeat.length - 1]?.down != true)}
|
||||
|
||||
{#if servicesDown?.length}
|
||||
<div class="section outage red">
|
||||
<div class="hasicon">
|
||||
<svg viewBox="0 0 768 768">{@html icons.warning}</svg>
|
||||
<div>
|
||||
<div class="title">
|
||||
{translate('there are services down', language)}
|
||||
</div>
|
||||
{#each servicesDown as service}
|
||||
<div class="service">
|
||||
{service.name[language] || service.name.en}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if servicesUp?.length}
|
||||
<div class="section up" class:green={!servicesDown?.length}>
|
||||
{#if servicesDown.length}
|
||||
<div class="title">
|
||||
{translate('services up', language)}
|
||||
</div>
|
||||
|
||||
{#each servicesUp as service}
|
||||
<div class="service">
|
||||
{service.name[language] || service.name.en}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="hasicon">
|
||||
<svg viewBox="0 0 768 768">{@html icons.check}</svg>
|
||||
<div>
|
||||
<div class="title">
|
||||
{translate('all services are up', language)}
|
||||
</div>
|
||||
|
||||
{#each servicesUp as service}
|
||||
<div class="service">
|
||||
{service.name[language] || service.name.en}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:catch}
|
||||
<div class="error">
|
||||
{translate('Encountered an error while fetching web service data.', language)}
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
<style>
|
||||
.section {
|
||||
padding: 1em;
|
||||
}
|
||||
.section.red {
|
||||
background-color: #980000;
|
||||
color: #fff;
|
||||
}
|
||||
.section.green {
|
||||
color: #007000;
|
||||
}
|
||||
|
||||
.hasicon {
|
||||
display: flex;
|
||||
gap: 2em;
|
||||
align-items: center;
|
||||
}
|
||||
.hasicon svg {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
.red svg :global(*) {
|
||||
fill: #fff;
|
||||
}
|
||||
.green svg :global(*) {
|
||||
fill: #007000;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
opacity: .7;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.section.outage .service {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user