// dtp-site-app.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const DTP_COMPONENT = { name: 'Site Admin', slug: 'site-admin-app' }; const dtp = window.dtp = window.dtp || { }; const GRID_COLOR = '#a0a0a0'; const GRID_TICK_COLOR = '#707070'; const AXIS_TICK_COLOR = '#c0c0c0'; const CHART_LINE_USER = 'rgb(0, 192, 0)'; const CHART_LINE_NICE = 'rgb(160, 160, 160)'; const CHART_LINE_SYSTEM = 'rgb(192, 192, 0)'; const CHART_LINE_IRQ = 'rgb(0, 0, 192)'; const CHART_LINE_TX_SEC = 'rgb(240, 185, 6)'; const CHART_LINE_RX_SEC = 'rgb(6, 154, 240)'; import DtpApp from 'dtp/dtp-app.js'; import numeral from 'numeral'; import UIkit from 'uikit'; // import UIkit from 'uikit'; export default class DtpSiteAdminHostStatsApp extends DtpApp { constructor (user) { super(DTP_COMPONENT, user); this.log.debug('constructor', 'app instance created'); } prepareGraphData ( ) { this.charts = { cpus: [ ], interfaces: [ ] }; this.coreGraphs = document.querySelectorAll('.dtp-cpu-graph'); this.coreCount = dtp.hostStats[0].cpus.length; this.cores = [ ]; for (let idx = 0; idx < this.coreCount; ++idx) { this.cores.push([ ]); } this.ifaceGraphs = document.querySelectorAll('.dtp-iface-graph'); this.ifaceCount = this.ifaceGraphs.length; this.ifaces = { }; dtp.hostStats[0].network.forEach((iface) => { this.ifaces[iface.iface] = [ ]; }); dtp.hostStats.forEach((stats) => { for (let idx = 0; idx < this.coreCount; ++idx) { const stat = stats.cpus[idx]; stat.totalTime = stat.user + stat.nice + stat.sys + stat.idle + stat.irq; this.cores[idx].push({ created: stats.created, ...stats.cpus[idx], }); } stats.network.forEach((iface) => { iface.created = stats.created; this.ifaces[iface.iface].push(iface); }); }); } renderCpuGraphs ( ) { for (let idx = 0; idx < this.coreCount; ++idx) { const ctx = this.coreGraphs[idx].getContext('2d'); const datasets = [ { label: 'user', data: this.cores[idx].map((sample) => ((sample.user / sample.totalTime) * 100.0)), borderColor: CHART_LINE_USER, tension: 0.5, }, { label: 'nice', data: this.cores[idx].map((sample) => ((sample.nice / sample.totalTime) * 100.0)), borderColor: CHART_LINE_NICE, tension: 0.5, }, { label: 'sys', data: this.cores[idx].map((sample) => ((sample.sys / sample.totalTime) * 100.0)), borderColor: CHART_LINE_SYSTEM, tension: 0.5, }, { label: 'irq', data: this.cores[idx].map((sample) => ((sample.irq / sample.totalTime) * 100.0)), borderColor: CHART_LINE_IRQ, tension: 0.5, }, ]; const chart = new Chart(ctx, { type: 'line', data: { labels: this.cores[idx].map((sample) => sample.created), datasets, }, options: { scales: { yAxis: { display: true, max: 100.0, ticks: { color: AXIS_TICK_COLOR, }, grid: { color: GRID_COLOR, tickColor: GRID_TICK_COLOR, }, }, xAxis: { display: false, }, }, plugins: { title: { display: false }, subtitle: { display: false }, legend: { display: true, position: 'bottom', }, }, }, }); this.charts.cpus.push(chart); } } renderNetworkGraphs ( ) { const ifNames = Object.keys(this.ifaces); ifNames.forEach((ifName) => { const iface = this.ifaces[ifName]; const canvas = document.querySelector(`.dtp-iface-graph[data-iface="${ifName}"]`); if (!canvas) { return; } const ctx = canvas.getContext('2d'); const datasets = [ { label: 'TX/sec', data: iface.map((sample) => sample.txPerSecond), borderColor: CHART_LINE_TX_SEC, tension: 0.5, }, { label: 'RX/sec', data: iface.map((sample) => sample.rxPerSecond), borderColor: CHART_LINE_RX_SEC, tension: 0.5, }, ]; const chart = new Chart(ctx, { type: 'line', data: { labels: iface.map((sample) => sample.created), datasets, }, options: { scales: { yAxis: { display: true, ticks: { color: AXIS_TICK_COLOR, callback: (value) => { let label = 'Mbps'; let megabits = value * (8 / 1024.0 / 1000.0); if (megabits > 1000) { label = 'Gbps'; megabits /= 1000.0; } return `${numeral(megabits).format('0,0.00')} ${label}`; }, }, grid: { color: GRID_COLOR, tickColor: GRID_TICK_COLOR, }, }, xAxis: { display: false }, }, plugins: { title: { display: false }, subtitle: { display: false }, legend: { display: true, position: 'bottom', }, }, }, }); this.charts.interfaces.push(chart); }); } async jobQueueAction (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const queueName = target.getAttribute('data-job-queue'); const jobId = target.getAttribute('data-job-id'); const action = target.getAttribute('data-job-action'); try { this.log.info('queueJobRemove', 'removing job from queue', { queueName, jobId }); const response = await fetch(`/admin/job-queue/${queueName}/${jobId}/action`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action }), }); if (!response.ok) { throw new Error('Server error'); } const json = await response.json(); if (!json.success) { throw new Error(json.message); } if (['remove','discard'].includes(action)) { window.location = `/admin/job-queue/${queueName}`; } } catch (error) { UIkit.modal.alert(`Failed to remove job: ${error.message}`); } } async sendNewsletter (event) { const newsletterId = event.currentTarget.getAttribute('data-newsletter-id'); const newsletterTitle = event.currentTarget.getAttribute('data-newsletter-title'); console.log(newsletterId, newsletterTitle); try { await UIkit.modal.confirm(`Are you sure you want to transmit "${newsletterTitle}"`); } catch (error) { this.log.info('sendNewsletter', 'aborted'); return; } try { const response = await fetch(`/admin/newsletter/${newsletterId}/transmit`, { method: 'POST', }); if (!response.ok) { throw new Error('Server error'); } await this.processResponse(response); } catch (error) { this.log.error('sendNewsletter', 'failed to send newsletter', { newsletterId, newsletterTitle, error }); UIkit.modal.alert(`Failed to send newsletter: ${error.message}`); } } async deleteNewsletter (event) { const newsletterId = event.currentTarget.getAttribute('data-newsletter-id'); const newsletterTitle = event.currentTarget.getAttribute('data-newsletter-title'); console.log(newsletterId, newsletterTitle); try { await UIkit.modal.confirm(`Are you sure you want to delete "${newsletterTitle}"`); } catch (error) { this.log.info('deleteNewsletter', 'aborted'); return; } try { const response = await fetch(`/admin/newsletter/${newsletterId}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete newsletter'); } await this.processResponse(response); } catch (error) { this.log.error('deleteNewsletter', 'failed to delete newsletter', { newsletterId, newsletterTitle, error }); UIkit.modal.alert(`Failed to delete newsletter: ${error.message}`); } } async deleteAnnouncement (event) { const target = event.currentTarget || event.target; const announcementId = target.getAttribute('data-announcement-id'); try { await UIkit.modal.confirm('Are you sure you want to delete the announcement?'); } catch (error) { return; } try { const actionUrl = `/admin/announcement/${announcementId}`; const response = await fetch(actionUrl, { method: 'DELETE' }); if (!response.ok) { throw new Error('Server error'); } await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to delete announcement: ${error.message}`); } } async postCoreConnectResponse (event, action) { const target = event.currentTarget || event.target; const requestId = target.getAttribute('data-request-id'); try { this.log.info('postCoreConnectResponse', 'posting Core Connect response', { requestId, action }); const requestUrl = `/admin/service-node/connect-queue/${requestId}`; const response = await fetch(requestUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action }), }); await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to send Core Connect response: ${error.message}`); } } async deleteServiceNode (event) { const target = event.currentTarget || event.target; const serviceNodeId = target.getAttribute('data-service-node-id'); try { await UIkit.modal.confirm('Are you sure you want to delete the Service Node?'); } catch (error) { return; // user cancel } try { const actionUrl = `/admin/service-node/${serviceNodeId}`; const response = await fetch(actionUrl, { method: 'DELETE' }); if (!response.ok) { throw new Error('Server error'); } await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to delete Service Node: ${error.message}`); } } async resolveNewsroomFeed (event) { event.preventDefault(); event.stopPropagation(); const form = document.querySelector('form#add-feed-form'); const input = form.querySelector('input#url'); const feedUrl = input.value; try { const response = await fetch(`/admin/newsroom/resolve`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ feedUrl }), }); await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to resolve feed: ${error.message}`); } return false; } async removeNewsroomFeed (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const feedId = target.getAttribute('data-feed-id'); const feedTitle = target.getAttribute('data-feed-title'); try { await UIkit.modal.confirm(`Are you sure you want to remove feed "${feedTitle}"?`); } catch (error) { // canceled return false; } try { const response = await fetch(`/admin/newsroom/${feedId}`, { method: 'DELETE' }); await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to remove feed: ${error.message}`); } return false; } async deletePost (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const postId = target.getAttribute('data-post-id'); const postTitle = target.getAttribute('data-post-title'); try { await UIkit.modal.confirm(`Are you sure you want to remove post "${postTitle}"?`); } catch (error) { // canceled return false; } try { const response = await fetch(`/admin/post/${postId}`, { method: 'DELETE' }); await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to remove post: ${error.message}`); } return false; } async deletePage (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const pageId = target.getAttribute('data-page-id'); const pageTitle = target.getAttribute('data-page-title'); try { await UIkit.modal.confirm(`Are you sure you want to remove page "${pageTitle}"?`); } catch (error) { // canceled return false; } try { const response = await fetch(`/admin/page/${pageId}`, { method: 'DELETE' }); await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to remove page: ${error.message}`); } return false; } async deleteSiteLink (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const link = JSON.parse(target.getAttribute('data-link')); try { await UIkit.modal.confirm(`Are you sure you want to remove site link "${link.label}"?`); } catch (error) { // canceled return false; } try { const response = await fetch(`/admin/site-link/${link._id}`, { method: 'DELETE' }); await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to remove site link: ${error.message}`); } return false; } async deleteVenueChannel (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const channel = JSON.parse(target.getAttribute('data-channel')); try { await UIkit.modal.confirm(`Are you sure you want to remove channel "${channel.name}"?`); } catch (error) { // canceled return false; } try { const response = await fetch(`/admin/venue/channel/${channel._id}`, { method: 'DELETE' }); await this.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to remove site link: ${error.message}`); } return false; } async submitImageForm (event) { event.preventDefault(); event.stopPropagation(); const formElement = event.currentTarget || event.target; const form = new FormData(formElement); this.cropper.getCroppedCanvas().toBlob(async (imageData) => { try { form.append('imageFile', imageData, 'icon.png'); this.log.info('submitImageForm', 'updating site image', { event, action: formElement.action }); const response = await fetch(formElement.action, { method: formElement.method, body: form, }); if (!response.ok) { let json; try { json = await response.json(); } catch (error) { throw new Error('Server error'); } throw new Error(json.message || 'Server error'); } await this.processResponse(response); window.location.reload(); } catch (error) { UIkit.modal.alert(`Failed to update site image: ${error.message}`); } }); return; } } dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp;