Files
PrivateCaptcha/web/layouts/settings-usage/scripts.html
2025-11-23 08:46:46 +02:00

275 lines
12 KiB
HTML

<script type="text/javascript">
if (typeof ChartComponent === 'undefined') {
class ChartComponent {
constructor(usageLimit) {
this.usageLimit = usageLimit;
this.isLoading = false;
this.chart = null;
// Constants
this.yTicksCount = 7;
this.maxBarWidth = 7;
this.backgroundColor = '#e4e4e7';
this.requestedColor = '#188B8B'; // pcteal-600
this.verifiedColor = '#F45D5D'; // pcred-300
this.grayColor = "#6b7280";
this.monthlyFormat = d3.timeFormat("%b");
}
monthlyTicks(date, i) {
const day = date.getDate();
const month = date.getMonth();
if (day === 1 && month === 0) {
return date.getFullYear();
} else {
return this.monthlyFormat(date);
}
}
yTickFormat(d) {
if (d >= 1_000_000_000) return (d / 1_000_000_000) + 'B';
if (d >= 1_000_000) return (d / 1_000_000) + 'M';
if (d >= 1_000) return (d / 1_000) + 'K';
return d;
}
drawNoData(element, xTickFunction, periodLengthDays) {
const margin = { top: 20, right: 30, bottom: 60, left: 30 };
const rect = element.getBoundingClientRect();
const width = rect.width - margin.left - margin.right;
const height = rect.height - margin.top - margin.bottom;
const d3Selection = d3.select(element);
d3Selection.selectAll('svg').remove();
const svg = d3Selection.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const chartElement = svg.append('g')
.attr('class', 'charts')
.attr("transform", `translate(${margin.left},${margin.top})`);
const x = d3.scaleTime().range([0, width]);
x.domain([d3.timeDay.offset(new Date(), -periodLengthDays), new Date()]);
const xTickValues = x.domain().filter((_, i) => !(i % 2));
const xAxis = d3.axisBottom(x)
.tickValues(xTickValues)
.tickFormat(xTickFunction);
const y = d3.scaleLinear().range([height, 0]);
chartElement.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
.style("color", this.backgroundColor)
.style("stroke-width", 2)
.selectAll("text")
.style("text-anchor", "end")
.style("color", "#000")
.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "rotate(-90)");
chartElement.append('g')
.call(d3.axisLeft(y).ticks(this.yTicksCount).tickSize(-width).tickFormat(''))
.style("color", this.backgroundColor)
.selectAll(".domain").remove();
chartElement.append("g")
.attr("transform", `translate(${width / 2 - 80},${height / 2 + 5})`)
.append("text")
.text("No data available")
.style("font-size", "20px")
.style("fill", this.grayColor);
}
setBarAttributes(bars, x, y, height, color) {
bars.enter().append("rect")
.attr("class", "bar")
.attr("x", d => x(d.x) + x.bandwidth() / 2)
.attr("width", () => Math.min(x.bandwidth(), this.maxBarWidth))
.attr("y", d => y(d.y))
.attr("height", d => (d.y > 1e-6 ? height - y(d.y) : 0))
.attr("rx", 3)
.attr("ry", 3)
.attr("fill", color)
.attr("opacity", 1)
.on("mouseover", function () { d3.select(this).attr("opacity", 0.8); })
.on("mouseout", function () { d3.select(this).attr("opacity", 1); })
.append("title").text(d => d.y);
}
setLegend(legend, text, color) {
legend.append("circle")
.attr("cx", -16)
.attr("cy", 0)
.attr("r", 6)
.style("fill", color);
legend.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("dy", ".35em")
.text(text)
.attr("class", "textselected")
.style("text-anchor", "start")
.style("font-size", "14px");
}
setChartData(element, data, xTickFormat) {
const requested = data.data;
requested.forEach(d => { d.x = new Date(d.x * 1000); });
const legendHeight = 50;
const margin = { top: 20, right: 30, bottom: 30, left: 30 };
const rect = element.getBoundingClientRect();
const width = rect.width - margin.left - margin.right;
const height = rect.height - legendHeight - margin.top - margin.bottom;
const d3Selection = d3.select(element);
d3Selection.selectAll('svg').remove();
const svg = d3Selection.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + legendHeight + margin.top + margin.bottom);
const chartElement = svg.append('g')
.attr('class', 'charts')
.attr("transform", `translate(${margin.left},${margin.top})`);
const x = d3.scaleBand().rangeRound([0, width]).padding(0.1);
const y = d3.scaleLinear().range([height, 0]);
x.domain(requested.map(d => d.x));
y.domain([0, d3.max(requested, d => d.y) * 1.2]);
const xTickValues = x.domain();
const xAxis = d3.axisBottom(x)
.tickValues(xTickValues)
.tickFormat(xTickFormat);
const yAxis = d3.axisLeft(y)
.ticks(this.yTicksCount)
.tickFormat(this.yTickFormat)
.tickPadding(5);
const yGrid = chartElement.append("g")
.attr("class", "grid")
.call(yAxis.tickSize(-width))
.style("color", this.backgroundColor);
yGrid.selectAll("text").style("color", this.grayColor);
yGrid.selectAll(".domain").remove();
if (this.usageLimit) {
const closeToLimit = requested.some(d => (1.1 * d.y) >= this.usageLimit);
if (closeToLimit) {
svg.append('line')
.attr('x1', 0)
.attr('x2', width)
.attr('y1', y(this.usageLimit))
.attr('y2', y(this.usageLimit))
.attr("transform", `translate(${margin.left},${margin.top})`)
.attr('stroke', this.verifiedColor)
.attr('stroke-width', 1)
.attr('stroke-dasharray', '5,5');
}
}
const barsRequested = chartElement.selectAll("bar-requested").data(requested);
this.setBarAttributes(barsRequested, x, y, height, this.requestedColor);
chartElement.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0,${height})`)
.call(xAxis)
.style("color", this.backgroundColor)
.style("stroke-width", 2)
.selectAll("text")
.style("text-anchor", "end")
.style("color", "#000")
.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "rotate(-90)");
const legendParent = chartElement.append("g")
.attr("class", "legendParent")
.attr("transform", `translate(${width / 2},${height + 30 + legendHeight / 2})`);
const legend1 = legendParent.append("g")
.attr("class", "legend-requested");
this.setLegend(legend1, 'Requests', this.requestedColor);
}
async fetchChartData(spinnerElement, maxRetries = 3, baseDelay = 1000) {
if (spinnerElement) { spinnerElement.style.display = 'flex'; }
try {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const response = await fetch('{{ partsURL $.Const.UserEndpoint $.Const.Stats | safeJS }}');
if (response.ok) {
return await response.json();
}
if (response.status === 429 || response.status === 503) {
const retryDelay = baseDelay * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, retryDelay));
continue;
}
const errorText = await response.text();
throw new Error(`Request failed (${response.status}): ${errorText || response.statusText}`);
}
} catch (error) {
console.error('Error fetching chart data:', error);
return null;
} finally {
if (spinnerElement) { spinnerElement.style.display = 'none'; }
}
}
async updateChart(chartElement, spinnerElement) {
const response = await this.fetchChartData(spinnerElement);
if (response && response.data && response.data.length > 0) {
this.setChartData(chartElement, response, this.monthlyTicks.bind(this));
const totalElement = document.getElementById("totalRequests");
if (totalElement) {
const sum = response.data.reduce((sum, item) => sum + item.y, 0);
const formatter = new Intl.NumberFormat('en', {
notation: 'compact',
compactDisplay: 'short',
});
totalElement.innerHTML = formatter.format(sum);;
}
} else {
this.drawNoData(chartElement, this.monthlyTicks.bind(this), 365);
totalElement.innerHTML = "N/A";
}
}
async init(chartElement, spinnerElement) {
await this.updateChart(chartElement, spinnerElement);
}
}
window.ChartComponent = ChartComponent;
}
(function(){
loadScript("{{$.Ctx.CDN}}/portal/js/d3.v7.min.js",
function() {
const chart = new ChartComponent({{ if $.Params.Limit }}{{$.Params.Limit}}{{else}}null{{end}}); // Pass usageLimit
chart.init(document.querySelector('#usage-chart'), document.querySelector('#usage-spinner')); // Chart container element
});
})()
</script>