Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
Add Metrcs Management web UI (#26)
Browse files Browse the repository at this point in the history
* feat(ui): Add support of dark theme in the Rules Management page

Signed-off-by: hayk96 <[email protected]>

* feat(ui): Add support of filtering rules by type

Signed-off-by: hayk96 <[email protected]>

* feat(ui): Display filename that currently in edit mode

Signed-off-by: hayk96 <[email protected]>

* feat(ui): Add Metrics Management webpage

Signed-off-by: hayk96 <[email protected]>

* feat(ui): Add Metrics Management icon in the /rules-management

Signed-off-by: hayk96 <[email protected]>

* feat(api): Add routes for Metrics Management page

Signed-off-by: hayk96 <[email protected]>

* chore(api): Bump app version

Signed-off-by: hayk96 <[email protected]>

* docs: Update CHANGELOG.md #patch

Signed-off-by: hayk96 <[email protected]>

---------

Signed-off-by: hayk96 <[email protected]>
  • Loading branch information
hayk96 authored Jun 1, 2024
1 parent 218df29 commit 4daa8bb
Show file tree
Hide file tree
Showing 10 changed files with 1,130 additions and 42 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.3.1 / 2024-06-01

* [ENHANCEMENT] Added a new webpage, Metrics Management, based on the `/metrics-lifecycle-policies` API. This feature allows
for directly defining and managing policies for retaining Prometheus metrics. #23
* [ENHANCEMENT] Added support for dark mode on the Rules Management page. #16
* [ENHANCEMENT] Added support of filtering of rules by their type from the UI. #15

## 0.3.0 / 2024-05-26

* [ENHANCEMENT]
Expand Down
23 changes: 23 additions & 0 deletions src/api/v1/endpoints/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

if arg_parser().get("web.enable_ui") == "true":
rules_management = "ui/rules-management"
metrics_management = "ui/metrics-management"
logger.info("Starting web management UI")

@router.get("/", response_class=HTMLResponse,
Expand Down Expand Up @@ -38,3 +39,25 @@ async def rules_management_files(path, request: Request):
"method": request.method,
"request_path": request.url.path})
return f"{sts} {msg}"

@router.get("/metrics-management",
description="RRenders metrics management HTML page of this application",
include_in_schema=False)
async def metrics_management_page():
return FileResponse(f"{metrics_management}/index.html")

@router.get(
"/metrics-management/{path}",
description="Returns JavaScript and CSS files of the metrics management page",
include_in_schema=False)
async def metrics_management_files(path, request: Request):
if path in ["script.js", "style.css"]:
return FileResponse(f"{metrics_management}/{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/utils/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def openapi(app: FastAPI):
"providing additional features and addressing its limitations. "
"Running as a sidecar alongside the Prometheus server enables "
"users to extend the capabilities of the API.",
version="0.3.0",
version="0.3.1",
contact={
"name": "Hayk Davtyan",
"url": "https://hayk96.github.io",
Expand Down
4 changes: 4 additions & 0 deletions ui/homepage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,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>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
Expand All @@ -178,6 +179,9 @@ <h1>The easiest Prometheus management interface</h1>
document.getElementById('rulesManagementButton').onclick = function() {
window.location.href = window.location.origin + '/rules-management';
};
document.getElementById('metricsManagementButton').onclick = function() {
window.location.href = window.location.origin + '/metrics-management';
};
});
</script>
</body>
Expand Down
67 changes: 67 additions & 0 deletions ui/metrics-management/index.html

Large diffs are not rendered by default.

308 changes: 308 additions & 0 deletions ui/metrics-management/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
document.addEventListener('DOMContentLoaded', () => {
setupEventListeners();
fetchAndDisplayPolicies();
});

function setupEventListeners() {
document.getElementById('createPolicyBtn').addEventListener('click', openCreatePolicyModal);
document.getElementById('submitNewPolicy').addEventListener('click', createPolicy);
document.getElementById('cancelCreatePolicyBtn').addEventListener('click', closeCreatePolicyModal);
document.getElementById('cancelEditPolicyBtn').addEventListener('click', closeEditPolicyModal);
document.getElementById('submitEditPolicy').addEventListener('click', savePolicy);
document.getElementById('confirmDeletePolicyBtn').addEventListener('click', confirmDeletePolicy);
document.getElementById('cancelDeletePolicyBtn').addEventListener('click', closeDeletePolicyModal);
document.getElementById('searchInput').addEventListener('input', handleSearchInput);
document.getElementById('homeBtn').addEventListener('click', () => window.location.href = '/');
document.getElementById('prometheusBtn').addEventListener('click', () => window.location.href = '/graph');
}

let allPolicies = [];
let policyToDelete = null;

/**
* This function is responsible for retrieving the
* current set of metrics lifecycle policies from
* the server and displaying them on the user interface
*/
function fetchAndDisplayPolicies() {
fetch('/api/v1/metrics-lifecycle-policies')
.then(response => response.json())
.then(data => {
allPolicies = data;
displayPolicies(data);
})
.catch(error => console.error('Error fetching policies:', error));
}

/**
* This function is responsible for
* rendering the metrics lifecycle
* policies onto the user interface
*/
function displayPolicies(policies) {
const policiesListElement = document.getElementById('policiesList');
policiesListElement.innerHTML = '';

if (Object.keys(policies).length === 0) {
const noPoliciesMessage = document.createElement('div');
noPoliciesMessage.className = 'no-policies-message';
noPoliciesMessage.textContent = 'No metrics lifecycle policies are defined';
policiesListElement.appendChild(noPoliciesMessage);
return;
}

for (const [name, policy] of Object.entries(policies)) {
const policyItem = document.createElement('div');
policyItem.className = 'policy-item';

const nameDiv = document.createElement('div');
nameDiv.textContent = name;
nameDiv.className = 'filename';
policyItem.appendChild(nameDiv);

const detailsDiv = document.createElement('div');
detailsDiv.className = 'details';


for (const [key, value] of Object.entries(policy)) {
const fieldDiv = document.createElement('div');
fieldDiv.className = `field-${key}`;
fieldDiv.innerHTML = `<span class="field-name">${capitalizeFirstLetter(key)}:</span> <span title="${value}">${value}</span>`;
detailsDiv.appendChild(fieldDiv);
}

policyItem.appendChild(detailsDiv);

const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'button-container';

const editButton = document.createElement('button');
editButton.className = 'edit-policy-btn';
editButton.textContent = 'Edit';
editButton.addEventListener('click', () => openEditPolicyModal(name, policy));
buttonsContainer.appendChild(editButton);

const deleteButton = document.createElement('button');
deleteButton.className = 'remove-policy-btn';
deleteButton.textContent = 'Delete';
deleteButton.addEventListener('click', () => openDeletePolicyModal(name));
buttonsContainer.appendChild(deleteButton);

policyItem.appendChild(buttonsContainer);
policiesListElement.appendChild(policyItem);
}
}

/**
* This function is designed to take a string
* as input and return a new string with the
* first letter capitalized and the rest of
* the string unchanged
*/
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1).replace(/_/g, ' ');
}

/**
* This function is an event handler designed
* to filter and display policies based on
* user input in the search bar
*/
function handleSearchInput(event) {
const searchTerm = event.target.value.toLowerCase();
const filteredPolicies = {};

for (const [name, policy] of Object.entries(allPolicies)) {
if (policy.match.toLowerCase().includes(searchTerm)) {
filteredPolicies[name] = policy;
}
}

displayPolicies(filteredPolicies);
}

/**
* This function is designed to handle
* the user interaction for opening the
* modal dialog used to create a new policy
*/
function openCreatePolicyModal() {
document.getElementById('createPolicyModal').style.display = 'block';
}

/**
* This function is responsible for handling
* the user interaction to close the create
* policy modal dialog
*/
function closeCreatePolicyModal() {
document.getElementById('createPolicyModal').style.display = 'none';
clearCreatePolicyForm();
}

/**
* This function is designed to reset and clear
* all input fields and error messages within
* the "Create New Policy" modal dialog
*/
function clearCreatePolicyForm() {
document.getElementById('newPolicyName').value = '';
document.getElementById('newPolicyMatch').value = '';
document.getElementById('newPolicyKeepFor').value = '';
document.getElementById('newPolicyDescription').value = '';
document.getElementById('createPolicyError').textContent = '';
}

/**
* This function is responsible for creating a new
* metrics lifecycle policy based on the input
* provided by the user in the "Create New Policy"
* modal dialog
*/
function createPolicy() {
const name = document.getElementById('newPolicyName').value.trim();
const match = document.getElementById('newPolicyMatch').value.trim();
const keepFor = document.getElementById('newPolicyKeepFor').value.trim();
const description = document.getElementById('newPolicyDescription').value.trim();


if (!name || !match || !keepFor) {
document.getElementById('createPolicyError').textContent = 'Policy name, match pattern, and retention period are required.';
return;
}

const policy = { name, match, keep_for: keepFor, description };

fetch('/api/v1/metrics-lifecycle-policies', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(policy)
})
.then(response => response.json().then(data => ({ ok: response.ok, data })))
.then(({ ok, data }) => {
if (ok) {
closeCreatePolicyModal();
fetchAndDisplayPolicies();
} else {
throw new Error(data.message || 'An error occurred');
}
})
.catch(error => {
document.getElementById('createPolicyError').textContent = `Error creating policy: ${error.message}`;
});
}

/**
* This function is responsible for opening
* the modal dialog to edit an existing
* metrics lifecycle policy
*/
function openEditPolicyModal(name, policy) {
document.getElementById('editPolicyName').value = name;
document.getElementById('editPolicyMatch').value = policy.match;
document.getElementById('editPolicyKeepFor').value = policy.keep_for;
document.getElementById('editPolicyDescription').value = policy.description;
document.getElementById('editPolicyModal').style.display = 'block';
}

/**
* This function is responsible for closing
* the edit policy modal dialog and clearing
* any input fields within the modal
*/
function closeEditPolicyModal() {
document.getElementById('editPolicyModal').style.display = 'none';
clearEditPolicyForm();
}

/**
* This function is designed to reset the
* input fields and clear any error messages
* in the edit policy modal.
*/
function clearEditPolicyForm() {
document.getElementById('editPolicyName').value = '';
document.getElementById('editPolicyMatch').value = '';
document.getElementById('editPolicyKeepFor').value = '';
document.getElementById('editPolicyDescription').value = '';
document.getElementById('editPolicyError').textContent = '';
}

/**
* This function is responsible for saving
* the changes made to an existing policy
*/
function savePolicy() {
const name = document.getElementById('editPolicyName').value.trim();
const match = document.getElementById('editPolicyMatch').value.trim();
const keepFor = document.getElementById('editPolicyKeepFor').value.trim();
const description = document.getElementById('editPolicyDescription').value.trim();

const policy = { match, keep_for: keepFor, description };

fetch(`/api/v1/metrics-lifecycle-policies/${name}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(policy)
})
.then(response => {
if (response.ok) {
closeEditPolicyModal();
fetchAndDisplayPolicies();
} else {
return response.json().then(data => {
throw new Error(data.message);
});
}
})
.catch(error => {
document.getElementById('editPolicyError').textContent = `Error saving policy: ${error.message}`;
});
}

/**
* This function is responsible for opening the delete
* policy modal and setting up the necessary information
* to confirm the deletion of a specified policy
*/
function openDeletePolicyModal(name) {
policyToDelete = name;
document.getElementById('deletePolicyModal').style.display = 'block';
}

/**
* This function is responsible for closing
* the delete policy modal and resetting
* any relevant state or information.
*/
function closeDeletePolicyModal() {
document.getElementById('deletePolicyModal').style.display = 'none';
policyToDelete = null;
}

/**
* This function is responsible for deleting a policy
* when the user confirms the deletion action
*/
function confirmDeletePolicy() {
if (policyToDelete) {
fetch(`/api/v1/metrics-lifecycle-policies/${policyToDelete}`, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
closeDeletePolicyModal();
fetchAndDisplayPolicies();
} else {
return response.json().then(data => {
throw new Error(data.message);
});
}
})
.catch(error => console.error('Error deleting policy:', error));
}
}
Loading

0 comments on commit 4daa8bb

Please sign in to comment.