Skip to content

Commit

Permalink
feature(ui): Add Reporting page web UI (#43)
Browse files Browse the repository at this point in the history
* feature(ui): Add Reporting page web UI

Signed-off-by: hayk96 <hayko5999@gmail.com>

* fix(api): Fix exception handling of /export API

Signed-off-by: hayk96 <hayko5999@gmail.com>

---------

Signed-off-by: hayk96 <hayko5999@gmail.com>
  • Loading branch information
hayk96 authored Jun 30, 2024
1 parent be76c88 commit 33bd467
Show file tree
Hide file tree
Showing 8 changed files with 506 additions and 2 deletions.
25 changes: 24 additions & 1 deletion src/api/v1/endpoints/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
if arg_parser().get("web.enable_ui") == "true":
rules_management = "ui/rules-management"
metrics_management = "ui/metrics-management"
reports = "ui/reports"
logger.info("Starting web management UI")

@router.get("/", response_class=HTMLResponse,
Expand Down Expand Up @@ -41,7 +42,7 @@ async def rules_management_files(path, request: Request):
return f"{sts} {msg}"

@router.get("/metrics-management",
description="RRenders metrics management HTML page of this application",
description="Renders metrics management HTML page of this application",
include_in_schema=False)
async def metrics_management_page():
return FileResponse(f"{metrics_management}/index.html")
Expand All @@ -61,3 +62,25 @@ async def metrics_management_files(path, request: Request):
"method": request.method,
"request_path": request.url.path})
return f"{sts} {msg}"

@router.get("/reports",
description="Renders Reports HTML page of this application",
include_in_schema=False)
async def metrics_management_page():
return FileResponse(f"{reports}/index.html")

@router.get(
"/reports/{path}",
description="Returns JavaScript and CSS files of the Reports",
include_in_schema=False)
async def metrics_management_files(path, request: Request):
if path in ["script.js", "style.css"]:
return FileResponse(f"{reports}/{path}")
sts, msg = "404", "Not Found"
logger.info(
msg=msg,
extra={
"status": sts,
"method": request.method,
"request_path": request.url.path})
return f"{sts} {msg}"
2 changes: 1 addition & 1 deletion src/core/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def replace_fields(data, custom_fields) -> None:
data[data.index(source_field)] = target_field
elif isinstance(data, dict):
data[target_field] = data.pop(source_field)
except KeyError:
except (ValueError, KeyError):
pass


Expand Down
5 changes: 5 additions & 0 deletions ui/homepage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
color: #ffffff;
background-image: linear-gradient(45deg, #f6d365, #fda085);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
width: 200px;
}
button:hover {
animation: buttonPulse 0.5s ease;
Expand Down Expand Up @@ -170,6 +171,7 @@ <h1>The easiest Prometheus management interface</h1>
<button id="openPrometheusButton">Open Prometheus</button>
<button id="rulesManagementButton">Rules Management</button>
<button id="metricsManagementButton">Metrics Management</button>
<button id="reportsButton">Reports</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
Expand All @@ -183,6 +185,9 @@ <h1>The easiest Prometheus management interface</h1>
document.getElementById('metricsManagementButton').onclick = function() {
window.location.href = window.location.origin + '/metrics-management';
};
document.getElementById('reportsButton').onclick = function() {
window.location.href = window.location.origin + '/reports';
};
});
</script>
</body>
Expand Down
4 changes: 4 additions & 0 deletions ui/metrics-management/index.html

Large diffs are not rendered by default.

92 changes: 92 additions & 0 deletions ui/reports/index.html

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions ui/reports/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
document.addEventListener('DOMContentLoaded', function() {
const modal = document.getElementById("exportModal");
const btn = document.getElementById("openModalBtn");
const span = document.getElementsByClassName("close")[0];


modal.style.display = "none";

btn.onclick = function() {
modal.style.display = "flex";
}

span.onclick = function() {
modal.style.display = "none";
}

document.getElementById('exportForm').addEventListener('submit', async function(event) {
event.preventDefault();

const expr = document.getElementById('expr').value;
const start = document.getElementById('start').value ? new Date(document.getElementById('start').value).toISOString() : null;
const end = document.getElementById('end').value ? new Date(document.getElementById('end').value).toISOString() : null;
const step = document.getElementById('step').value;
const timestamp_format = document.getElementById('timestamp_format').value;
const format = document.getElementById('format').value;

const replaceFields = {};
document.querySelectorAll('.replace-field').forEach(field => {
const key = field.querySelector('.replace-key').value;
const value = field.querySelector('.replace-value').value;
if (key && value) {
replaceFields[key] = value;
}
});

const data = {
expr: expr,
start: start,
end: end,
step: step,
timestamp_format: timestamp_format,
replace_fields: replaceFields
};

try {
const response = await fetch('/api/v1/export?format=' + format, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});

if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}

const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `data.${format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
document.getElementById('results').innerHTML = `<p style="color: red;">${error.message}</p>`;
}
});

document.getElementById('addReplaceField').addEventListener('click', function() {
const container = document.getElementById('replaceFieldsContainer');
const newField = document.createElement('div');
newField.className = 'replace-field';
newField.innerHTML = `
<input type="text" class="replace-key" placeholder="Source field">
<input type="text" class="replace-value" placeholder="Target field">
<button type="button" class="remove-field">‒</button>
`;
container.appendChild(newField);
});

document.getElementById('replaceFieldsContainer').addEventListener('click', function(event) {
if (event.target.classList.contains('remove-field')) {
event.target.parentElement.remove();
}
});
});
Loading

0 comments on commit 33bd467

Please sign in to comment.