Feature/rework vue schema

This commit is contained in:
Luca Haid
2024-05-10 21:03:01 +00:00
parent 1f30671cf9
commit 78c9d3ef37
34 changed files with 2290 additions and 1146 deletions
+225
View File
@@ -0,0 +1,225 @@
Vue.component('Domain', {
//language=Vue
template: `
<div>
<!-- start page title -->
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=getDomains'" :config="domainsTableConfig"
small ssr ref="table">
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary" @click="reloadDomains">
<template v-if="reloadDomainsLoading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<template v-else>
<i class="fas fa-sync-alt"></i>
Reload Domains
</template>
</button>
<div class="input-group">
<input type="text" class="form-control" v-model="checkDomainInput" placeholder="Neue Domain überprüfen">
<div class="input-group-append">
<button class="btn btn-primary" @click="checkDomainAvailability">
<template v-if="checkDomainLoading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<template v-else>
<i class="fas fa-search"></i>
</template>
</button>
</div>
</div>
</template>
<!-- Slot to show DNS records button -->
<template v-slot:inwxroid="{ row }">
<button type="button" class="btn btn-primary" @click="showDnsRecordsModal(row.domain)"
:class="dnsRecordsModalLoading === row.domain ? 'disabled' : ''">
<template v-if="dnsRecordsModalLoading === row.domain">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<span v-else>DNS</span>
</button>
</template>
<!-- Registrant Admin Tech Billing from domainContacts -->
<template v-slot:registrant="{ row }">
{{ domainContacts[row.registrant] ? domainContacts[row.registrant]["name"] : '' }}
</template>
<template v-slot:admin="{ row }">{{ domainContacts[row.admin] ? domainContacts[row.admin]["name"] : ''}}
</template>
<template v-slot:tech="{ row }">{{ domainContacts[row.tech] ? domainContacts[row.tech]["name"] : ''}}
</template>
<template v-slot:billing="{ row }">{{ domainContacts[row.billing] ? domainContacts[row.billing]["name"] : ''}}
</template>
</tt-table>
<!-- Bootstrap Modal to query and show all DNS records for a domain -->
<div class="modal show d-block" tabindex="-1" role="dialog" style="background: rgba(0, 0, 0, 0.5);"
ref="dnsRecordsModal" @click="dnsRecordsModal.domain = null" @keydown.esc="dnsRecordsModal.domain = null"
v-if="dnsRecordsModal.domain">
<div class="modal-dialog" role="document" style="max-width: fit-content" @click.stop>
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">DNS Records for {{ dnsRecordsModal.domain ?? '' }}</h5>
<button type="button" class="close" @click="dnsRecordsModal.domain = null">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table class="tt-table table-striped table-bordered table-hover table-sm table-condensed">
<thead>
<tr>
<th>Record Class</th>
<th>Record Type</th>
<th>Record Host</th>
<th>Record Value</th>
<th>Record TTL</th>
</tr>
</thead>
<tbody>
<tr v-for="record in dnsRecordsModal.records">
<td>{{ record.class }}</td>
<td>{{ record.type }} {{ record.pri ? '(' + record.pri + ')' : '' }}</td>
<td>{{ record.host }}</td>
<td>{{ record.value }}</td>
<td>{{ record.ttl }}</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="dnsRecordsModal.domain = null">Close</button>
</div>
</div>
</div>
</div>
</div>
`,
data() {
return {
window: window,
domainContacts: {},
reloadDomainsLoading: false,
dnsRecordsModalLoading: null,
dnsRecordsModal: {
domain: null, records: []
},
checkDomainInput: '',
checkDomainResult: null,
checkDomainLoading: false
}
}, created() {
this.fetchDomainContacts().then()
}, computed: {
domainsTableConfig() {
const base = {
headers: [{text: "DNS", key: "inwxRoId", filter: false, sortable: false}, {
text: "Domain",
key: "domain"
}, {
text: "Plesk",
key: "pleskId",
filter: 'iconSelect',
filterOptions: [{value: 1, text: 'Yes', icon: 'fas fa-check text-success'}, {
value: 0,
text: 'No',
icon: 'fas fa-times text-danger'
}],
sortable: false
}, {text: "Created Date", key: "crDate", filter: "date"}, {
text: "Expiration Date",
key: "exDate",
filter: "date"
}, {text: "Renewal Date", key: "reDate", filter: "date"}, {
text: "Updated Date",
key: "upDate",
filter: "date"
}, {
text: "Transfer Lock",
key: "transferLock",
filter: 'iconSelect',
filterOptions: [{value: 1, text: 'Locked', icon: 'fas fa-lock text-danger'}, {
value: 0,
text: 'Unlocked',
icon: 'fas fa-unlock text-success'
}]
}, {text: "Authorization Code", key: "authCode", sortable: false}, {
text: "Registrant ID",
key: "registrant",
sortable: false
}, {text: "Admin ID", key: "admin", sortable: false}, {
text: "Tech ID",
key: "tech",
sortable: false
}, {text: "Billing ID", key: "billing", sortable: false}, {text: "Name Servers", key: "ns"}],
tableHeader: 'Domains',
key: 'Domain'
}
const domainContactsSorted = Object.entries(this.domainContacts).sort(([, a], [, b]) => a.name.localeCompare(b.name))
const domainContactsFilterOptions = domainContactsSorted.map(([, contact]) => {
return {text: contact.name, value: contact.inwxRoId}
})
// for registrant admin tech billing set filter to select with domainContacts if domainContacts is not empty
if (Object.keys(this.domainContacts).length > 0) {
base.headers = base.headers.map(header => {
if (['registrant', 'admin', 'tech', 'billing'].includes(header.key)) {
header.filter = 'select'
header.filterOptions = domainContactsFilterOptions
}
return header
})
}
return base
}
}, methods: {
async showDnsRecordsModal(domain) {
this.dnsRecordsModalLoading = domain
this.dnsRecordsModal = {
domain: null, records: []
}
const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=getDnsRecords&domain=' + domain)
this.dnsRecordsModal.domain = domain
this.dnsRecordsModal.records = response.data.map(record => {
if (typeof record.entries === 'object') {
record.value = record.entries[0]
} else {
record.value = record.target || record.txt || record.ip
}
if (record.type === 'SOA') {
record.value = record.mname + ' ' + record.rname + ' ' + record.serial + ' ' + record.refresh + ' ' + record.retry + ' ' + record.expire
}
return record
})
this.dnsRecordsModalLoading = null
this.$nextTick(() => {
this.$refs.dnsRecordsModal.focus()
})
}, async fetchDomainContacts() {
const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=getDomainContacts')
this.domainContacts = response.data
}, async reloadDomains() {
this.reloadDomainsLoading = true
const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=importAllDomains')
window.notify('success', response.data["importMessages"].join('<br>'))
await Promise.all([this.fetchDomainContacts(), this.$refs.table.fetchData(this.$refs.table.pagination.page)])
this.reloadDomainsLoading = false
}, //TODO: make this cleaner
async checkDomainAvailability() {
this.checkDomainLoading = true
const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=checkDomain&domain=' + this.checkDomainInput)
const priceInformation = response.data.price.domain[this.checkDomainInput]
window.notify(response.data.status === 'free' ? 'success' : 'error', `Domain ist ${response.data.status === 'free' ? 'verfügbar. Registrieren um' : 'nicht frei. Transfer um'} ${priceInformation.price}${priceInformation.currency}/${priceInformation.period === '1Y' ? 'Jahr' : priceInformation.period}`)
this.checkDomainLoading = false
}
}
})
@@ -0,0 +1,185 @@
Vue.component('HistoricTicket', {
//language=Vue
template: `
<div>
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['HISTORIC_TICKET_API_URL'] + '?do=getHistoricTickets'"
:config="historicTicketTableConfig"
small ssr ref="table">
<template v-slot:top-buttons>
<!-- add input for global search with label and bootstrap class-->
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Globale Suche" v-model="globalSearch"
@keydown.enter="doGlobalSearch">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="doGlobalSearch">Submit</button>
</div>
</div>
</template>
<template v-slot:first_name="{ row }">
{{ row.first_name }} {{ row.last_name }}
</template>
<template v-slot:ctime="{ row }">
{{ new Date(row.ctime * 1000).toLocaleString() }}
</template>
<template v-slot:ticket_number="{ row }">
<a href="#" @click="clickTicketNumber(row.ticket_number)">{{ row.ticket_number }}</a>
</template>
</tt-table>
<!-- Bootstrap Modal to show global search results -->
<div class="modal show d-block" tabindex="0" role="dialog" style="background: rgba(0, 0, 0, 0.5);"
@click="globalSearchModal = false" @keydown.esc="globalSearchModal = false" ref="globalSearchModal"
v-if="globalSearchModal">
<div class="modal-dialog" role="document" @click.stop
style="width:fit-content;max-width: 80vw ; max-height: 80vh; overflow-y: auto;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Suchergebnisse</h5>
<button type="button" class="close" @click="globalSearchModal = false">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<tt-table
:fetch-url="\`${window['TT_CONFIG']['HISTORIC_TICKET_API_URL']}?do=findHistoricTicket&query=\${globalSearch}\`"
:config="globalSearchModalTableConfig"
small ref="table">
<template v-slot:ctime="{ row }">
{{ window.moment(row.ctime * 1000).format('DD.MM.YYYY HH:mm') }}
</template>
<template v-slot:ticket_number="{ row }">
<a href="#" @click="clickTicketNumber(row.ticket_number)">{{ row.ticket_number }}</a>
</template>
</tt-table>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="globalSearchModal = false">Close</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap Modal to show ticket messages -->
<div class="modal show d-block" tabindex="0" role="dialog" style="background: rgba(0, 0, 0, 0.5);"
@click="selectedTicketNumber = null" @keydown.esc="selectedTicketNumber = null" ref="selectedTicketModal"
v-if="selectedTicketNumber">
<div class="modal-dialog" role="document" @click.stop
style="width:fit-content;max-width: 80vw ; max-height: 80vh; overflow-y: auto;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ticket {{ selectedTicketNumber }}</h5>
<button type="button" class="close" @click="selectedTicketNumber = null">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div v-if="selectedTicketData">
<h5>{{ selectedTicketData.ticket.subject }}</h5>
<p>{{ selectedTicketData.ticket.message }}</p>
<div v-for="message in selectedTicketData.messages">
<hr>
<h6>{{ new Date(message.ctime * 1000).toLocaleString()}}</h6>
<p style="word-break: break-all;" v-html="message.content?.replaceAll('\\n', '<br>')"></p>
</div>
</div>
<div v-else class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
`, data() {
return {
window: window,
selectedTicketNumber: null,
selectedTicketData: null,
globalSearch: '',
globalSearchModal: false,
globalSearchModalTableConfig: {
headers: [{text: 'Ticket Number', key: 'ticket_number', filter: false, sortable: false},
{text: 'Erstellt', key: 'ctime', filter: false, sortable: false},
{text: 'Subject', key: 'ticket_subject', filter: false, sortable: false},
{text: 'Message', key: 'ticket_message', filter: false, sortable: false},],
tableHeader: 'Suchergebnisse',
key: 'HistoricTicketGlobalSearch',
},
historicTicketTableConfig: {
headers: [{text: 'Ticket Number', key: 'ticket_number', filter: 'search'},
{text: 'Erstellt', key: 'ctime', filter: false},
{text: 'Subject', key: 'subject', filter: 'search', sortable: false},
{
text: 'Type',
key: 'type',
filter: 'select',
filterOptions: [{value: 'BACKOFFICE', text: 'BACKOFFICE'},
{value: 'KUNDENANFRAGEN', text: 'KUNDENANFRAGEN'},
{value: 'STÖRUNGEN', text: 'STÖRUNGEN'},
{value: 'ALLGEMEINES', text: 'ALLGEMEINES'},
{value: 'TERMIN VEREINBART', text: 'TERMIN VEREINBART'},
{value: 'VERRECHNEN AB DATUM', text: 'VERRECHNEN AB DATUM'},
{value: 'ONLINE-TICKETS', text: 'ONLINE-TICKETS'},
{value: 'KÜNDIGUNG', text: 'KÜNDIGUNG'},
{value: 'BESTELLUNGEN', text: 'BESTELLUNGEN'},
{value: 'PORTIERUNG', text: 'PORTIERUNG'},
{value: 'KABEL-TV', text: 'KABEL-TV'},
{value: 'TIEFBAU', text: 'TIEFBAU'},
{value: 'ENERGIE STEIERMARK', text: 'ENERGIE STEIERMARK'},
{value: 'INTERN', text: 'INTERN'},
{value: 'FELIX', text: 'FELIX'},
{value: '0', text: '0'},
{value: 'JETTEN', text: 'JETTEN'},]
},
{
text: 'Status',
key: 'status',
filter: 'select',
filterOptions: [{value: 'Geschlossen', text: 'Geschlossen'},
{value: 'In Evidenz', text: 'In Evidenz'},
{value: 'In Bearbeitung', text: 'In Bearbeitung'},
{value: 'Business In Bearbeitung', text: 'Business In Bearbeitung'},
{value: 'Business Angebot gelegt', text: 'Business Angebot gelegt'},]
},
{text: 'Name', key: 'first_name', filter: 'search', sortable: false},
{text: 'Email', key: 'email', filter: 'search', sortable: false},
{text: 'Phone', key: 'phone', filter: 'search', sortable: false},],
defaultPageSize: 25,
tableHeader: 'Historische Tickets',
key: 'HistoricTicket',
}
}
}, methods: {
async doGlobalSearch() {
if (this.globalSearch.length > 0) {
this.globalSearchModal = true;
this.$nextTick(() => {
this.$refs.globalSearchModal.focus();
});
}
}, async clickTicketNumber(ticketNumber) {
this.globalSearchModal = false;
this.selectedTicketData = null;
this.selectedTicketNumber = ticketNumber;
const response = await axios.post(`${window['TT_CONFIG']['HISTORIC_TICKET_API_URL']}?do=getHistoricTicketMessages`,
{ticketNumber});
this.selectedTicketData = response.data;
this.$nextTick(() => {
this.$refs.selectedTicketModal.focus();
});
}
}
})
+178
View File
@@ -0,0 +1,178 @@
Vue.component('IpNetwork', {
//language=Vue
template: `
<div>
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']"
:path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['IPNETWORK_API_URL'] + '?do=get'"
:config="IpNetworkTableConfig"
@row-click="(row) => row.cidr !== '32' && switchCurrentNetwork(row.id)"
@reset-table="switchCurrentNetwork"
small ssr disable-initial-fetch ref="table">
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary"
@click="switchCurrentNetwork(currentNetworkData.parent_network_id)"
:disabled="!currentNetworkData">
<i class="fas fa-sync-alt"></i>Go Back
</button>
<button type="button" class="btn btn-primary" @click="addModal = true">
<i class="fas fa-sync-alt"></i>Add new Network Space
</button>
</template>
<!-- add $slots.expandedRow to the table component and display discription -->
<template v-slot:expandedRow="{row}">
<span style="white-space: pre;" v-if="row.description" v-text="row.description"></span>
<span v-else>No description</span>
</template>
</tt-table>
<!-- add modal -->
<div class="modal show d-block" tabindex="-1" role="dialog" style="background: rgba(0, 0, 0, 0.5);"
ref="addModal" @click="addModal = false" @keydown.esc="addModal = false" v-if="addModal === true">
<div class="modal-dialog" role="document" @click.stop>
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit DNS Record</h5>
<button type="button" class="close" @click="addModal = false">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="wrapper" style="display:grid; grid-template-columns: 3fr 1fr 2fr; grid-gap: 12px">
<div class="form-group">
<label for="network_address">Network Address</label>
<input type="text" class="form-control" id="network_address"
v-model="addModalData.network_address">
</div>
<div class="form-group">
<label for="cidr">CIDR</label>
<input type="text" class="form-control" id="cidr" v-model="addModalData.cidr">
</div>
<div class="form-group">
<label for="status">Status</label>
<select class="form-control" id="status" v-model="addModalData.status">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="reserved">Reserved</option>
</select>
</div>
<div class="form-group" style="grid-column: span 2">
<label for="name_location">Name</label>
<input type="text" class="form-control" id="name_location"
v-model="addModalData.name">
</div>
<div class="form-group">
<label for="name_location">Location</label>
<input type="text" class="form-control" id="name_location"
v-model="addModalData.location">
</div>
<div class="form-group" style="grid-column: span 3">
<label for="description">Description</label>
<input type="text" class="form-control" id="description"
v-model="addModalData.description">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" @click="addSubmit">Save</button>
<button class="btn btn-secondary" @click="addModal = false">Close</button>
</div>
</div>
</div>
</div>
</div>
`,
data() {
return {
window: window,
apiUrl: window['TT_CONFIG']['IPNETWORK_API_URL'],
IpNetworkTableConfig: {
defaultPageSize: 50,
customRowClass: function (row) {
return row.cidr !== '32' ? 'tt-pointer' : '';
},
expandCondition: function (row) {
return !!row.description;
},
headers: [
{text: 'Network Address', key: 'network_address_str'},
{text: 'Name', key: 'name'},
{
text: 'Status', key: 'status', filter: 'iconSelect',
filterOptions: [{value: 'active', text: 'Active', icon: 'fas fa-check text-success'},
{value: 'inactive', text: 'Inactive', icon: 'fas fa-times text-danger'},
{value: 'reserved', text: 'Reserved', icon: 'fas fa-lock text-warning'}]
},
{text: 'Children', key: 'children', filter: 'numberRange'},
],
tableHeader: 'IPAM',
key: 'IpNetwork'
},
currentNetworkData: null,
addModal: false,
addModalData: {
network_address: '',
cidr: '',
parent_network_id: '',
status: 'active',
name: '',
description: '',
location: '',
},
}
},
async mounted() {
function popstateFunction() {
const parentNetworkId = new URLSearchParams(window.location.search).get('parent_network_id');
this.switchCurrentNetwork(parentNetworkId).then();
}
window.onpopstate = popstateFunction.bind(this);
window.onpopstate.call(this)
},
methods: {
async switchCurrentNetwork(networkId = null) {
if (!networkId) {
this.$refs.table.$set(this.$refs.table.filters, 'parent_network_id', undefined);
this.currentNetworkData = null;
this.IpNetworkTableConfig.tableHeader = 'IPAM';
this.$refs.table.disableDebounce = true;
window.history.pushState({}, '', `?`);
} else {
this.$refs.table.disableDebounce = true;
this.$refs.table.$set(this.$refs.table.filters, 'parent_network_id', networkId);
window.history.pushState({}, '', `?parent_network_id=${networkId}`);
const response = await axios.post(`${this.apiUrl}?do=getById`, {id: networkId});
this.currentNetworkData = response.data.network;
this.IpNetworkTableConfig.tableHeader = `IPAM - ${this.currentNetworkData.network_address_str}/${this.currentNetworkData.cidr} - ${this.currentNetworkData.name}`;
}
await this.$refs.table.fetchData();
},
async addSubmit() {
const response = await axios.post(`${this.apiUrl}?do=create`,
{
...this.addModalData,
parent_network_id: this.currentNetworkData ? this.currentNetworkData.id : null
});
if (response.data.status === 'success') {
this.addModal = false;
window.notify('success', 'Network space created successfully');
await this.$refs.table.fetchData();
} else {
window.notify('error', response.data.message);
}
},
},
})
@@ -0,0 +1,130 @@
Vue.filter('cleanupURL', function (value) {
value = value.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").split('/')[0];
return value;
})
Vue.component('RaspberryDisplay', {
//language=Vue
template: `
<div>
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<div class="card">
<tt-loader v-if="loading"></tt-loader>
<div class="p-2">
<h3>8322 Studenzen NOC Displays</h3>
<div class="display-grid">
<div v-for="display in displays" :key="display.id"
:class="['display', display['display_label'].includes('-B-') ? 'big-42-inch' : 'small-27-inch']"
:style="display['custom_style']" style="">
<div
style="display: grid; grid-template-columns: max-content auto max-content; justify-items: center;width:100%; padding: 0 2px">
<div>
<!-- FONT AWESOME ONLINE GREEN CIRCLE -->
<i class="fas fa-circle" data-toggle="tooltip" title="ONLINE" style="color: green"></i>
</div>
<div>
<div @click.prevent="enableDisplayURLEditMode(display.id)" style="cursor: pointer">
<span v-if="displaysURLEditMode !== display.id">{{ display['display_url'] | cleanupURL }}</span>
<input v-else-if="displaysURLEditMode === display.id"
v-model="display['display_url']"
@keyup.enter="disableDisplayURLEditMode(display.id, display['display_url'])"
@blur="disableDisplayURLEditMode(display.id, display['display_url'])"
ref="displayURLEditInput"
class="form-control"
type="text">
</div>
</div>
<div style="cursor: pointer">
<!-- FONT AWESOME REBOOT ICON -->
<i class="fas fa-red fa-sync-alt" data-toggle="tooltip" title="Reboot this Raspberry"
@click="rebootRaspberry(display.id)"
style="color: green"></i>
</div>
</div>
<div>
<!-- Checkbox for Auto Refresh Enabled -->
<div style="display: inline-block" data-toggle="tooltip"
:title="\`Auto refresh is \${display['auto_refresh_enabled'] ? 'enabled' : 'disabled'}.\`">
<input type="checkbox" :id="'auto_refresh_enabled_checkbox_' + display.id"
v-model="display['auto_refresh_enabled']"
@change="submitChanges(display.id, 'auto_refresh_enabled', display['auto_refresh_enabled'])">
<label :for="'auto_refresh_enabled_checkbox_' + display.id">ARF</label>
</div>
<!-- This will only display if both are true, consider adjusting logic as needed -->
<span style="margin: 0 4px"> | </span>
<!-- Checkbox for Margin Hotfix Enabled -->
<div style="display: inline-block" data-toggle="tooltip"
:title="\`Margin Hotfix is \${display['margin_hot_fix_enabled'] ? 'enabled' : 'disabled'}.\`">
<input type="checkbox" :id="'margin_hot_fix_enabled_checkbox_' + display.id"
v-model="display['margin_hot_fix_enabled']"
@change="submitChanges(display.id, 'margin_hot_fix_enabled', display['margin_hot_fix_enabled'])">
<label :for="'margin_hot_fix_enabled_checkbox_' + display.id">MHF</label>
</div>
</div>
<div v-text="display['display_label']"></div>
</div>
</div>
</div>
</div>
</div>
`,
data() {
return {
loading: false, displaysURLEditMode: null, displays: null, window: window
}
},
mounted() {
this.fetchDisplays().then()
}, methods: {
async rebootRaspberry(displayID) {
this.loading = true;
await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=reboot`, {
params: {
displayID: displayID
}
});
this.loading = false;
}, async fetchDisplays() {
this.loading = true;
const response = await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=getDisplays`);
this.displays = response.data.result;
this.loading = false;
Vue.nextTick(() => {
$('[data-toggle="tooltip"]').tooltip('dispose');
$('[data-toggle="tooltip"]').tooltip();
});
}, enableDisplayURLEditMode(displayID) {
this.displaysURLEditMode = displayID;
const _this = this;
// wait for the DOM to update
Vue.nextTick(() => {
_this.$refs['displayURLEditInput'][0].focus();
});
}, disableDisplayURLEditMode(displayID, displayURL) {
this.displaysURLEditMode = null;
this.submitChanges(displayID, 'display_url', displayURL);
}, async submitChanges(displayID, field, value) {
this.loading = true;
await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=change`, {
params: {
displayID: displayID, field: field, value: value,
}
});
await this.fetchDisplays();
this.loading = false;
}
},
})
@@ -0,0 +1,112 @@
Vue.component('VoiceCallActive', {
//language=Vue
template: `
<div>
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['VOICE_CALL_ACTIVE_API_URL'] + '?do=getActiveCalls'"
:config="VoiceCallActiveTableConfig"
small ref="table">
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary" @click="refresh" data-toggle="tooltip" data-placement="bottom"
title="Refreshing too often will run into API-Rate limits and will cause errors.">
<template v-if="refreshLoading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<template v-else>
<i class="fas fa-sync-alt"></i>
Refresh
</template>
</button>
<div class="d-flex">
<label style="margin-bottom: 0 !important;">
<input type="checkbox" id="autoRefresh" data-toggle="toggle" data-size="lg">
<span class="ml-2">Auto Refresh (5sec)</span>
</label>
<span style="width: 50px"></span>
<div class="voice-yellow p-2">Ringing</div>
<div class="voice-red p-2">Outgoing</div>
<div class="voice-green p-2">Ingoing</div>
</div>
</template>
<template v-slot:answer_time="{ row }">
{{ !isNaN(new Date(row.answer_time)) ? window.moment(row.answer_time, 'YYYY-MM-DD HH:mm:ss Z').format('DD.MM.YYYY HH:mm:ss') : 'Call is not running' }}
</template>
<template v-slot:status="{ row }">
<i v-if="!row.dst_device_extension && row.status !== 'Ringing'" class="fas fa-phone-arrow-up-right"></i>
<i v-else-if="!row.dst_device_extension && row.status === 'Ringing'"
class="fas fa-phone-arrow-up-right fa-shake"></i>
<i v-else-if="row.status === 'Ringing'" class="fas fa-phone-arrow-down-left fa-shake"></i>
<i v-else class="fas fa-phone-arrow-down-left"></i>
{{ row.status }}
</template>
</tt-table>
</div>
`,
data() {
return {
window: window,
VoiceCallActiveTableConfig: {
customRowClass: function (row) {
if (row.status.toLowerCase() === 'ringing') {
return 'voice-yellow';
}
if (!row.dst_device_extension) {
return 'voice-red';
}
if (row.status.toLowerCase() === 'answered') {
return 'voice-green';
}
},
headers: [
{text: 'Call ID', key: 'id', filter: false, sortable: false},
{text: 'Status', key: 'status', filter: false, sortable: false},
{text: 'Answer Time', key: 'answer_time', filter: false, sortable: false},
{text: 'Duration', key: 'duration', filter: false, sortable: false},
{text: 'Source', key: 'src', filter: false, sortable: false},
{text: 'Device Type', key: 'device_type', filter: false, sortable: false},
{text: 'Destination', key: 'localized_dst', filter: false, sortable: false},
{text: 'Destination User', key: 'dst_user', filter: false, sortable: false},
{text: 'Destination Device Extension', key: 'dst_device_extension', filter: false, sortable: false},
],
tableHeader: 'Active Voice Calls',
key: 'VoiceCallActive',
},
refreshLoading: false,
autoRefresh: null,
}
},
mounted() {
//TODO: create vue tooltip component
$('[data-toggle="tooltip"]').tooltip();
const _this = this;
$('#autoRefresh').change(function () {
console.log(this.checked);
if (this.checked) {
_this.autoRefresh = setInterval(function () {
_this.refresh();
}, 5000);
} else {
clearInterval(_this.autoRefresh);
}
})
},
methods: {
async refresh() {
this.refreshLoading = true;
this.$refs.table.loading = true;
await this.$refs.table.fetchData();
$('.tooltip').tooltip('hide');
this.$refs.table.loading = false;
this.refreshLoading = false;
},
}
})
@@ -0,0 +1,56 @@
Vue.component('VoiceCallHistory', {
//language=Vue
template: `
<div>
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['VOICE_CALL_HISTORY_API_URL'] + '?do=getCalls'"
:config="VoiceCallHistoryTableConfig"
small ssr ref="table">
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary" @click="importCallsFromToday">
<template v-if="importCallsFromTodayLoading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<template v-else>
<i class="fas fa-sync-alt"></i>
Re-Import Calls from Today
</template>
</button>
</template>
</tt-table>
</div>
`, data() {
return {
window: window,
VoiceCallHistoryTableConfig: {
headers: [{text: "Call-ID", key: "uid"},
{text: "Voice Account", key: "voice_account"},
{text: "Time Range", key: "start", filter: "date"},
{text: "Source", key: "source"},
{text: "Destination", key: "destination"},
{
text: "Billable",
key: "billable",
filter: "iconSelect",
filterOptions: [{value: 1, text: 'Yes', icon: 'fas fa-check text-success'},
{value: 0, text: 'No', icon: 'fas fa-times text-danger'}]
},
{text: "Duration", key: "duration", filter: "numberRange"},],
tableHeader: 'Voice Call History',
key: 'VoiceCallHistory',
},
importCallsFromTodayLoading: false,
}
},
methods: {
async importCallsFromToday() {
this.importCallsFromTodayLoading = true;
const response = await axios.get(window['TT_CONFIG']['VOICE_CALL_HISTORY_API_URL'] + '?do=importCallsFromToday');
window.notify(response.data.status === 'success' ? 'success' : 'error', response.data.message);
await this.$refs.table.fetchData();
this.importCallsFromTodayLoading = false;
},
}
})
-63
View File
@@ -1,63 +0,0 @@
// noinspection JSJQueryEfficiency
Vue.filter('cleanupURL', function (value) {
value = value.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").split('/')[0];
return value;
})
new Vue({
el: '#app',
mounted() {
this.fetchDisplays()
},
methods: {
async rebootRaspberry(displayID) {
this.loading = true;
await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=reboot`, {
params: {
displayID: displayID
}
});
this.loading = false;
},
async fetchDisplays() {
this.loading = true;
const response = await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=getDisplays`);
this.displays = response.data.result;
this.loading = false;
Vue.nextTick(() => {
$('[data-toggle="tooltip"]').tooltip('dispose');
$('[data-toggle="tooltip"]').tooltip();
});
},
enableDisplayURLEditMode(displayID) {
this.displaysURLEditMode = displayID;
const _this = this;
// wait for the DOM to update
Vue.nextTick(() => {
_this.$refs['displayURLEditInput'][0].focus();
});
},
disableDisplayURLEditMode(displayID, displayURL) {
this.displaysURLEditMode = null;
this.submitChanges(displayID, 'display_url', displayURL);
},
async submitChanges(displayID, field, value) {
this.loading = true;
await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=change`, {
params: {
displayID: displayID,
field: field,
value: value,
}
});
await this.fetchDisplays();
this.loading = false;
}
},
data: {
loading: false,
displaysURLEditMode: null,
displays: null,
}
});