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

Add Metrcs Management web UI #26

Merged
merged 8 commits into from
Jun 1, 2024
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
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
Loading