// If you want to use Phoenix channels, run `mix help phx.gen.channel` // to get started and then uncomment the line below. // import "./user_socket.js" // You can include dependencies in two ways. // // The simplest option is to put them in assets/vendor and // import them using relative paths: // // import "../vendor/some-package.js" // // Alternatively, you can `npm install some-package --prefix assets` and import // them using a path starting with the package name: // // import "some-package" // // If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file. // To load it, simply add a second `` to your `root.html.heex` file. // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. import "phoenix_html" // Establish Phoenix Socket and LiveView configuration. import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import {hooks as colocatedHooks} from "phoenix-colocated/towerops" import topbar from "../vendor/topbar" import Chart from "../vendor/chart" // Generic Sensor Chart Hook (for CPU, Memory, Storage, etc.) const SensorChart = { mounted() { this.createChart() }, updated() { // Destroy and recreate chart to properly handle scale changes if (this.chart) { this.chart.destroy() } this.createChart() }, destroyed() { if (this.chart) { this.chart.destroy() } }, createChart() { const data = JSON.parse(this.el.dataset.chart) const canvas = this.el.querySelector('canvas') const ctx = canvas.getContext('2d') const unit = this.el.dataset.unit || '%' const autoScale = this.el.dataset.autoScale === 'true' const showZeroLine = this.el.dataset.showZeroLine === 'true' // Generate colors for each dataset const colors = [ 'rgb(59, 130, 246)', // blue 'rgb(239, 68, 68)', // red 'rgb(34, 197, 94)', // green 'rgb(234, 179, 8)', // yellow 'rgb(168, 85, 247)', // purple 'rgb(236, 72, 153)', // pink 'rgb(14, 165, 233)', // sky 'rgb(249, 115, 22)', // orange ] const datasets = data.datasets.map((dataset, index) => ({ ...dataset, borderColor: colors[index % colors.length], backgroundColor: colors[index % colors.length].replace('rgb', 'rgba').replace(')', ', 0.1)'), borderWidth: 2, tension: 0.4, pointRadius: 0, pointHoverRadius: 4, })) // Y-axis configuration const yAxisConfig = autoScale ? { beginAtZero: showZeroLine, ticks: { callback: function(value) { return value + unit } }, grid: { color: function(context) { // Make the zero line more prominent if (showZeroLine && context.tick.value === 0) { return 'rgba(0, 0, 0, 0.3)' } return 'rgba(0, 0, 0, 0.05)' }, lineWidth: function(context) { // Make the zero line thicker if (showZeroLine && context.tick.value === 0) { return 2 } return 1 } } } : { min: 0, max: 100, ticks: { callback: function(value) { return value + unit } }, grid: { color: 'rgba(0, 0, 0, 0.05)' } } this.chart = new Chart(ctx, { type: 'line', data: { datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, plugins: { legend: { position: 'bottom', labels: { usePointStyle: true, padding: 15, } }, tooltip: { callbacks: { title: function(context) { const timestamp = context[0].parsed.x const date = new Date(timestamp) return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }) }, label: function(context) { return context.dataset.label + ': ' + context.parsed.y.toFixed(1) + unit } } } }, scales: { x: { type: 'linear', ticks: { callback: function(value, index) { const point = this.chart.data.datasets[0]?.data[index] if (!point) return '' const date = new Date(point.x) return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }) }, maxTicksLimit: 12 }, grid: { display: false } }, y: yAxisConfig } } }) } } const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") const liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: {_csrf_token: csrfToken}, hooks: {...colocatedHooks, SensorChart}, }) // Show progress bar on live navigation and form submits topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) // connect if there are any LiveViews on the page liveSocket.connect() // expose liveSocket on window for web console debug logs and latency simulation: // >> liveSocket.enableDebug() // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.disableLatencySim() window.liveSocket = liveSocket // The lines below enable quality of life phoenix_live_reload // development features: // // 1. stream server logs to the browser console // 2. click on elements to jump to their definitions in your code editor // if (process.env.NODE_ENV === "development") { window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => { // Enable server log streaming to client. // Disable with reloader.disableServerLogs() reloader.enableServerLogs() // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component // // * click with "c" key pressed to open at caller location // * click with "d" key pressed to open at function component definition location let keyDown window.addEventListener("keydown", e => keyDown = e.key) window.addEventListener("keyup", _e => keyDown = null) window.addEventListener("click", e => { if(keyDown === "c"){ e.preventDefault() e.stopImmediatePropagation() reloader.openEditorAtCaller(e.target) } else if(keyDown === "d"){ e.preventDefault() e.stopImmediatePropagation() reloader.openEditorAtDef(e.target) } }, true) window.liveReloader = reloader }) }