Feature/rework vue schema
This commit is contained in:
@@ -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">×</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">×</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">×</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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
@@ -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">×</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;
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user