Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

### Enhancements
* [[`PR-27`](https://github.com/thiagoesteves/observer_web/pull/27)] Adding Igniter.
* [[`PR-28`](https://github.com/thiagoesteves/observer_web/pull/28)] Updating Tailwind.
* [[`PR-28`](https://github.com/thiagoesteves/observer_web/pull/28)] Adding theme support.
* [[`PR-28`](https://github.com/thiagoesteves/observer_web/pull/28)] Assets organization.

## 0.1.11 🚀 (2025-08-29)

Expand Down
20 changes: 16 additions & 4 deletions assets/css/app.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
@import url('https://fonts.googleapis.com/css?family=Oswald&display=swap');

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@custom-variant dark (&:where(.dark, .dark *));

/* This file is for your main application CSS */
/* Tailwind */

@import "tailwindcss";
@plugin "@tailwindcss/forms";

@source "../../lib/**/*.*ex";

@theme {
--font-family-sans: "Oswald", sans-serif;
--font-family-mono: "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;

--spacing-72: 18rem;
--spacing-84: 21rem;
--spacing-96: 24rem;
}
156 changes: 33 additions & 123 deletions assets/js/app.js
Original file line number Diff line number Diff line change
@@ -1,137 +1,37 @@
// 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"
//

// Establish Phoenix Socket and LiveView configuration.
// Phoenix assets are imported from dependencies
import topbar from "topbar"
import * as echarts from "echarts"

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveTran = document.querySelector("meta[name='live-transport']").getAttribute("content")
const livePath = document.querySelector("meta[name='live-path']").getAttribute("content")

let hooks = {}

hooks.ScrollBottom = {
mounted() {
this.el.scrollTo(0, this.el.scrollHeight);
},
import { loadAll } from "./lib/settings";

updated() {
const pixelsBelowBottom =
this.el.scrollHeight - this.el.clientHeight - this.el.scrollTop;
import LiveMetricsEChart from "./hooks/live_metrics_echart";
import ObserverEChart from "./hooks/observer_echart";
import ScrollBottom from "./hooks/scroll_bottom";
import Themer from "./hooks/themer";

if (pixelsBelowBottom < this.el.clientHeight * 0.3) {
this.el.scrollTo(0, this.el.scrollHeight);
}
},
const hooks = {
LiveMetricsEChart,
ObserverEChart,
ScrollBottom,
Themer,
};

hooks.ObserverEChart = {
mounted() {
selector = "#" + this.el.id

this.chart = echarts.init(this.el.querySelector(selector + "-chart"))
option = JSON.parse(this.el.querySelector(selector + "-data").textContent)

this.chart.setOption(option)
},
updated() {
selector = "#" + this.el.id
// This flag will indicate to Echart to not merge the data
let notMerge = !this.el.dataset.merge ?? true;
// Topbar ---

newOption = JSON.parse(this.el.querySelector(selector + "-data").textContent)

// Compare the new option series with the previous one
if (this.previousSeries && JSON.stringify(this.previousSeries) === JSON.stringify(newOption.series)) {
// If the data is the same, skip the update
console.log('No changes in the data, skipping setOption');
return; // Exit without updating the chart
}
let topBarScheduled = undefined;

// Save the new option as the previous one for future comparisons
this.previousSeries = newOption.series;

// Set the callback in the tooltip formatter (or any other part of the option)
var callback = (args) => {
this.pushEventTo(this.el, "request-process", { id: args.data.id, series_name: args.seriesName });
return args.data.id;
}

newOption.tooltip = {
formatter: callback
};

this.chart.setOption(newOption, notMerge)
}
};
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })

hooks.LiveMetricsEChart = {
mounted() {
selector = "#" + this.el.id

const dataConfig = JSON.parse(this.el.dataset.config)
const columns = JSON.parse(this.el.dataset.columns)

this.chart = echarts.init(this.el.querySelector(selector + "-chart"))
this.chart.setOption(dataConfig)
this.graph_cols = columns
},
updated() {
const dataConfig = JSON.parse(this.el.dataset.config)
const reset = JSON.parse(this.el.dataset.reset)
const columns = JSON.parse(this.el.dataset.columns)

if (reset) {
this.chart.setOption(dataConfig)

} else {
var option = this.chart.getOption();
var updatedXAxis = option.xAxis[0].data.concat(dataConfig.xAxis.data);
var updatedSeries = option.series.map((series, index) => {
// Concatenate the corresponding dataset to each series
return {
data: series.data.concat(dataConfig.series[index] ? dataConfig.series[index].data : [])
};
});

this.chart.setOption(
{
xAxis: { data: updatedXAxis },
series: updatedSeries
})
}
if (columns != this.columns) {
this.chart.resize()
this.columns = columns
}
window.addEventListener("phx:page-loading-start", (info) => {
if (!topBarScheduled) {
topBarScheduled = setTimeout(() => topbar.show(), 500);
}
};

const liveSocket = new LiveView.LiveSocket(livePath, Phoenix.Socket, {
transport: liveTran === "longpoll" ? Phoenix.LongPoll : WebSocket,
params: { _csrf_token: csrfToken },
hooks
})
});

// 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())
window.addEventListener("phx:page-loading-stop", (info) => {
clearTimeout(topBarScheduled);
topBarScheduled = undefined;
topbar.hide();
});

window.addEventListener("phx:copy_to_clipboard", event => {
if ("clipboard" in navigator) {
Expand Down Expand Up @@ -164,6 +64,16 @@ window.addEventListener("phx:copy_to_clipboard", event => {
}, 1000);
});

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveTran = document.querySelector("meta[name='live-transport']").getAttribute("content")
const livePath = document.querySelector("meta[name='live-path']").getAttribute("content")

const liveSocket = new LiveView.LiveSocket(livePath, Phoenix.Socket, {
transport: liveTran === "longpoll" ? Phoenix.LongPoll : WebSocket,
params: { _csrf_token: csrfToken, init_state: loadAll() },
hooks
})

// connect if there are any LiveViews on the page
liveSocket.connect()

Expand Down
45 changes: 45 additions & 0 deletions assets/js/hooks/live_metrics_echart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as echarts from "echarts"

const LiveMetricsEChart = {
mounted() {
selector = "#" + this.el.id

const dataConfig = JSON.parse(this.el.dataset.config)
const columns = JSON.parse(this.el.dataset.columns)

this.chart = echarts.init(this.el.querySelector(selector + "-chart"))
this.chart.setOption(dataConfig)
this.graph_cols = columns
},
updated() {
const dataConfig = JSON.parse(this.el.dataset.config)
const reset = JSON.parse(this.el.dataset.reset)
const columns = JSON.parse(this.el.dataset.columns)

if (reset) {
this.chart.setOption(dataConfig)

} else {
var option = this.chart.getOption();
var updatedXAxis = option.xAxis[0].data.concat(dataConfig.xAxis.data);
var updatedSeries = option.series.map((series, index) => {
// Concatenate the corresponding dataset to each series
return {
data: series.data.concat(dataConfig.series[index] ? dataConfig.series[index].data : [])
};
});

this.chart.setOption(
{
xAxis: { data: updatedXAxis },
series: updatedSeries
})
}
if (columns != this.columns) {
this.chart.resize()
this.columns = columns
}
}
};

export default LiveMetricsEChart
43 changes: 43 additions & 0 deletions assets/js/hooks/observer_echart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as echarts from "echarts"

const ObserverEChart = {
mounted() {
selector = "#" + this.el.id

this.chart = echarts.init(this.el.querySelector(selector + "-chart"))
option = JSON.parse(this.el.querySelector(selector + "-data").textContent)

this.chart.setOption(option)
},
updated() {
selector = "#" + this.el.id
// This flag will indicate to Echart to not merge the data
let notMerge = !this.el.dataset.merge ?? true;

newOption = JSON.parse(this.el.querySelector(selector + "-data").textContent)

// Compare the new option series with the previous one
if (this.previousSeries && JSON.stringify(this.previousSeries) === JSON.stringify(newOption.series)) {
// If the data is the same, skip the update
console.log('No changes in the data, skipping setOption');
return; // Exit without updating the chart
}

// Save the new option as the previous one for future comparisons
this.previousSeries = newOption.series;

// Set the callback in the tooltip formatter (or any other part of the option)
var callback = (args) => {
this.pushEventTo(this.el, "request-process", { id: args.data.id, series_name: args.seriesName });
return args.data.id;
}

newOption.tooltip = {
formatter: callback
};

this.chart.setOption(newOption, notMerge)
}
};

export default ObserverEChart
16 changes: 16 additions & 0 deletions assets/js/hooks/scroll_bottom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const ScrollBottom = {
mounted() {
this.el.scrollTo(0, this.el.scrollHeight);
},

updated() {
const pixelsBelowBottom =
this.el.scrollHeight - this.el.clientHeight - this.el.scrollTop;

if (pixelsBelowBottom < this.el.clientHeight * 0.3) {
this.el.scrollTo(0, this.el.scrollHeight);
}
},
};

export default ScrollBottom
24 changes: 24 additions & 0 deletions assets/js/hooks/themer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { load, store } from "../lib/settings"

const Themer = {
applyTheme() {
const wantsDark = window.matchMedia("(prefers-color-scheme: dark)").matches
const theme = load("theme")

if (theme === "dark" || (theme === "system" && wantsDark) || (!theme && wantsDark)) {
document.documentElement.classList.add("dark")
} else {
document.documentElement.classList.remove("dark")
}
},

mounted() {
this.handleEvent("update-theme", ({ theme }) => {
store("theme", theme)

this.applyTheme()
})
},
}

export default Themer
21 changes: 21 additions & 0 deletions assets/js/lib/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const PREFIX = "observer:"

export function loadAll() {
const values = {}

for (const [key, json] of Object.entries(localStorage)) {
if (key.startsWith(PREFIX)) values[key] = JSON.parse(json)
}

return values
}

export function load(key) {
const json = localStorage.getItem(PREFIX + key)

if (json) return JSON.parse(json)
}

export function store(key, value) {
localStorage.setItem(PREFIX + key, JSON.stringify(value))
}
Loading