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

Commit 5bf9d5d

Browse files
authored
Add Metrcs Management web UI (#24)
* feat(ui): Add support of dark theme in the Rules Management page Signed-off-by: hayk96 <hayko5999@gmail.com> * feat(ui): Add support of filtering rules by type Signed-off-by: hayk96 <hayko5999@gmail.com> * feat(ui): Display filename that currently in edit mode Signed-off-by: hayk96 <hayko5999@gmail.com> * feat(ui): Add Metrics Management webpage Signed-off-by: hayk96 <hayko5999@gmail.com> * feat(ui): Add Metrics Management icon in the /rules-management Signed-off-by: hayk96 <hayko5999@gmail.com> * feat(api): Add routes for Metrics Management page Signed-off-by: hayk96 <hayko5999@gmail.com> * chore(api): Bump app version Signed-off-by: hayk96 <hayko5999@gmail.com> * docs: Update CHANGELOG.md Signed-off-by: hayk96 <hayko5999@gmail.com> * docs: Update CHANGELOG.md #patch Signed-off-by: hayk96 <hayko5999@gmail.com> --------- Signed-off-by: hayk96 <hayko5999@gmail.com>
1 parent c0bb260 commit 5bf9d5d

File tree

10 files changed

+1130
-42
lines changed

10 files changed

+1130
-42
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 0.3.1 / 2024-06-01
4+
5+
* [ENHANCEMENT] Added a new webpage, Metrics Management, based on the `/metrics-lifecycle-policies` API. This feature allows
6+
for directly defining and managing policies for retaining Prometheus metrics. #23
7+
* [ENHANCEMENT] Added support for dark mode on the Rules Management page. #16
8+
* [ENHANCEMENT] Added support of filtering of rules by their type from the UI. #15
9+
310
## 0.3.0 / 2024-05-26
411

512
* [ENHANCEMENT]

src/api/v1/endpoints/web.py

+23
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

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

1415
@router.get("/", response_class=HTMLResponse,
@@ -38,3 +39,25 @@ async def rules_management_files(path, request: Request):
3839
"method": request.method,
3940
"request_path": request.url.path})
4041
return f"{sts} {msg}"
42+
43+
@router.get("/metrics-management",
44+
description="RRenders metrics management HTML page of this application",
45+
include_in_schema=False)
46+
async def metrics_management_page():
47+
return FileResponse(f"{metrics_management}/index.html")
48+
49+
@router.get(
50+
"/metrics-management/{path}",
51+
description="Returns JavaScript and CSS files of the metrics management page",
52+
include_in_schema=False)
53+
async def metrics_management_files(path, request: Request):
54+
if path in ["script.js", "style.css"]:
55+
return FileResponse(f"{metrics_management}/{path}")
56+
sts, msg = "404", "Not Found"
57+
logger.info(
58+
msg=msg,
59+
extra={
60+
"status": sts,
61+
"method": request.method,
62+
"request_path": request.url.path})
63+
return f"{sts} {msg}"

src/utils/openapi.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def openapi(app: FastAPI):
1616
"providing additional features and addressing its limitations. "
1717
"Running as a sidecar alongside the Prometheus server enables "
1818
"users to extend the capabilities of the API.",
19-
version="0.3.0",
19+
version="0.3.1",
2020
contact={
2121
"name": "Hayk Davtyan",
2222
"url": "https://hayk96.github.io",

ui/homepage/index.html

+4
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
<h1>The easiest Prometheus management interface</h1>
169169
<button id="openPrometheusButton">Open Prometheus</button>
170170
<button id="rulesManagementButton">Rules Management</button>
171+
<button id="metricsManagementButton">Metrics Management</button>
171172
</div>
172173
<script>
173174
document.addEventListener('DOMContentLoaded', function() {
@@ -178,6 +179,9 @@ <h1>The easiest Prometheus management interface</h1>
178179
document.getElementById('rulesManagementButton').onclick = function() {
179180
window.location.href = window.location.origin + '/rules-management';
180181
};
182+
document.getElementById('metricsManagementButton').onclick = function() {
183+
window.location.href = window.location.origin + '/metrics-management';
184+
};
181185
});
182186
</script>
183187
</body>

ui/metrics-management/index.html

+67
Large diffs are not rendered by default.

ui/metrics-management/script.js

+308
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
document.addEventListener('DOMContentLoaded', () => {
2+
setupEventListeners();
3+
fetchAndDisplayPolicies();
4+
});
5+
6+
function setupEventListeners() {
7+
document.getElementById('createPolicyBtn').addEventListener('click', openCreatePolicyModal);
8+
document.getElementById('submitNewPolicy').addEventListener('click', createPolicy);
9+
document.getElementById('cancelCreatePolicyBtn').addEventListener('click', closeCreatePolicyModal);
10+
document.getElementById('cancelEditPolicyBtn').addEventListener('click', closeEditPolicyModal);
11+
document.getElementById('submitEditPolicy').addEventListener('click', savePolicy);
12+
document.getElementById('confirmDeletePolicyBtn').addEventListener('click', confirmDeletePolicy);
13+
document.getElementById('cancelDeletePolicyBtn').addEventListener('click', closeDeletePolicyModal);
14+
document.getElementById('searchInput').addEventListener('input', handleSearchInput);
15+
document.getElementById('homeBtn').addEventListener('click', () => window.location.href = '/');
16+
document.getElementById('prometheusBtn').addEventListener('click', () => window.location.href = '/graph');
17+
}
18+
19+
let allPolicies = [];
20+
let policyToDelete = null;
21+
22+
/**
23+
* This function is responsible for retrieving the
24+
* current set of metrics lifecycle policies from
25+
* the server and displaying them on the user interface
26+
*/
27+
function fetchAndDisplayPolicies() {
28+
fetch('/api/v1/metrics-lifecycle-policies')
29+
.then(response => response.json())
30+
.then(data => {
31+
allPolicies = data;
32+
displayPolicies(data);
33+
})
34+
.catch(error => console.error('Error fetching policies:', error));
35+
}
36+
37+
/**
38+
* This function is responsible for
39+
* rendering the metrics lifecycle
40+
* policies onto the user interface
41+
*/
42+
function displayPolicies(policies) {
43+
const policiesListElement = document.getElementById('policiesList');
44+
policiesListElement.innerHTML = '';
45+
46+
if (Object.keys(policies).length === 0) {
47+
const noPoliciesMessage = document.createElement('div');
48+
noPoliciesMessage.className = 'no-policies-message';
49+
noPoliciesMessage.textContent = 'No metrics lifecycle policies are defined';
50+
policiesListElement.appendChild(noPoliciesMessage);
51+
return;
52+
}
53+
54+
for (const [name, policy] of Object.entries(policies)) {
55+
const policyItem = document.createElement('div');
56+
policyItem.className = 'policy-item';
57+
58+
const nameDiv = document.createElement('div');
59+
nameDiv.textContent = name;
60+
nameDiv.className = 'filename';
61+
policyItem.appendChild(nameDiv);
62+
63+
const detailsDiv = document.createElement('div');
64+
detailsDiv.className = 'details';
65+
66+
67+
for (const [key, value] of Object.entries(policy)) {
68+
const fieldDiv = document.createElement('div');
69+
fieldDiv.className = `field-${key}`;
70+
fieldDiv.innerHTML = `<span class="field-name">${capitalizeFirstLetter(key)}:</span> <span title="${value}">${value}</span>`;
71+
detailsDiv.appendChild(fieldDiv);
72+
}
73+
74+
policyItem.appendChild(detailsDiv);
75+
76+
const buttonsContainer = document.createElement('div');
77+
buttonsContainer.className = 'button-container';
78+
79+
const editButton = document.createElement('button');
80+
editButton.className = 'edit-policy-btn';
81+
editButton.textContent = 'Edit';
82+
editButton.addEventListener('click', () => openEditPolicyModal(name, policy));
83+
buttonsContainer.appendChild(editButton);
84+
85+
const deleteButton = document.createElement('button');
86+
deleteButton.className = 'remove-policy-btn';
87+
deleteButton.textContent = 'Delete';
88+
deleteButton.addEventListener('click', () => openDeletePolicyModal(name));
89+
buttonsContainer.appendChild(deleteButton);
90+
91+
policyItem.appendChild(buttonsContainer);
92+
policiesListElement.appendChild(policyItem);
93+
}
94+
}
95+
96+
/**
97+
* This function is designed to take a string
98+
* as input and return a new string with the
99+
* first letter capitalized and the rest of
100+
* the string unchanged
101+
*/
102+
function capitalizeFirstLetter(string) {
103+
return string.charAt(0).toUpperCase() + string.slice(1).replace(/_/g, ' ');
104+
}
105+
106+
/**
107+
* This function is an event handler designed
108+
* to filter and display policies based on
109+
* user input in the search bar
110+
*/
111+
function handleSearchInput(event) {
112+
const searchTerm = event.target.value.toLowerCase();
113+
const filteredPolicies = {};
114+
115+
for (const [name, policy] of Object.entries(allPolicies)) {
116+
if (policy.match.toLowerCase().includes(searchTerm)) {
117+
filteredPolicies[name] = policy;
118+
}
119+
}
120+
121+
displayPolicies(filteredPolicies);
122+
}
123+
124+
/**
125+
* This function is designed to handle
126+
* the user interaction for opening the
127+
* modal dialog used to create a new policy
128+
*/
129+
function openCreatePolicyModal() {
130+
document.getElementById('createPolicyModal').style.display = 'block';
131+
}
132+
133+
/**
134+
* This function is responsible for handling
135+
* the user interaction to close the create
136+
* policy modal dialog
137+
*/
138+
function closeCreatePolicyModal() {
139+
document.getElementById('createPolicyModal').style.display = 'none';
140+
clearCreatePolicyForm();
141+
}
142+
143+
/**
144+
* This function is designed to reset and clear
145+
* all input fields and error messages within
146+
* the "Create New Policy" modal dialog
147+
*/
148+
function clearCreatePolicyForm() {
149+
document.getElementById('newPolicyName').value = '';
150+
document.getElementById('newPolicyMatch').value = '';
151+
document.getElementById('newPolicyKeepFor').value = '';
152+
document.getElementById('newPolicyDescription').value = '';
153+
document.getElementById('createPolicyError').textContent = '';
154+
}
155+
156+
/**
157+
* This function is responsible for creating a new
158+
* metrics lifecycle policy based on the input
159+
* provided by the user in the "Create New Policy"
160+
* modal dialog
161+
*/
162+
function createPolicy() {
163+
const name = document.getElementById('newPolicyName').value.trim();
164+
const match = document.getElementById('newPolicyMatch').value.trim();
165+
const keepFor = document.getElementById('newPolicyKeepFor').value.trim();
166+
const description = document.getElementById('newPolicyDescription').value.trim();
167+
168+
169+
if (!name || !match || !keepFor) {
170+
document.getElementById('createPolicyError').textContent = 'Policy name, match pattern, and retention period are required.';
171+
return;
172+
}
173+
174+
const policy = { name, match, keep_for: keepFor, description };
175+
176+
fetch('/api/v1/metrics-lifecycle-policies', {
177+
method: 'POST',
178+
headers: {
179+
'Content-Type': 'application/json'
180+
},
181+
body: JSON.stringify(policy)
182+
})
183+
.then(response => response.json().then(data => ({ ok: response.ok, data })))
184+
.then(({ ok, data }) => {
185+
if (ok) {
186+
closeCreatePolicyModal();
187+
fetchAndDisplayPolicies();
188+
} else {
189+
throw new Error(data.message || 'An error occurred');
190+
}
191+
})
192+
.catch(error => {
193+
document.getElementById('createPolicyError').textContent = `Error creating policy: ${error.message}`;
194+
});
195+
}
196+
197+
/**
198+
* This function is responsible for opening
199+
* the modal dialog to edit an existing
200+
* metrics lifecycle policy
201+
*/
202+
function openEditPolicyModal(name, policy) {
203+
document.getElementById('editPolicyName').value = name;
204+
document.getElementById('editPolicyMatch').value = policy.match;
205+
document.getElementById('editPolicyKeepFor').value = policy.keep_for;
206+
document.getElementById('editPolicyDescription').value = policy.description;
207+
document.getElementById('editPolicyModal').style.display = 'block';
208+
}
209+
210+
/**
211+
* This function is responsible for closing
212+
* the edit policy modal dialog and clearing
213+
* any input fields within the modal
214+
*/
215+
function closeEditPolicyModal() {
216+
document.getElementById('editPolicyModal').style.display = 'none';
217+
clearEditPolicyForm();
218+
}
219+
220+
/**
221+
* This function is designed to reset the
222+
* input fields and clear any error messages
223+
* in the edit policy modal.
224+
*/
225+
function clearEditPolicyForm() {
226+
document.getElementById('editPolicyName').value = '';
227+
document.getElementById('editPolicyMatch').value = '';
228+
document.getElementById('editPolicyKeepFor').value = '';
229+
document.getElementById('editPolicyDescription').value = '';
230+
document.getElementById('editPolicyError').textContent = '';
231+
}
232+
233+
/**
234+
* This function is responsible for saving
235+
* the changes made to an existing policy
236+
*/
237+
function savePolicy() {
238+
const name = document.getElementById('editPolicyName').value.trim();
239+
const match = document.getElementById('editPolicyMatch').value.trim();
240+
const keepFor = document.getElementById('editPolicyKeepFor').value.trim();
241+
const description = document.getElementById('editPolicyDescription').value.trim();
242+
243+
const policy = { match, keep_for: keepFor, description };
244+
245+
fetch(`/api/v1/metrics-lifecycle-policies/${name}`, {
246+
method: 'PATCH',
247+
headers: {
248+
'Content-Type': 'application/json'
249+
},
250+
body: JSON.stringify(policy)
251+
})
252+
.then(response => {
253+
if (response.ok) {
254+
closeEditPolicyModal();
255+
fetchAndDisplayPolicies();
256+
} else {
257+
return response.json().then(data => {
258+
throw new Error(data.message);
259+
});
260+
}
261+
})
262+
.catch(error => {
263+
document.getElementById('editPolicyError').textContent = `Error saving policy: ${error.message}`;
264+
});
265+
}
266+
267+
/**
268+
* This function is responsible for opening the delete
269+
* policy modal and setting up the necessary information
270+
* to confirm the deletion of a specified policy
271+
*/
272+
function openDeletePolicyModal(name) {
273+
policyToDelete = name;
274+
document.getElementById('deletePolicyModal').style.display = 'block';
275+
}
276+
277+
/**
278+
* This function is responsible for closing
279+
* the delete policy modal and resetting
280+
* any relevant state or information.
281+
*/
282+
function closeDeletePolicyModal() {
283+
document.getElementById('deletePolicyModal').style.display = 'none';
284+
policyToDelete = null;
285+
}
286+
287+
/**
288+
* This function is responsible for deleting a policy
289+
* when the user confirms the deletion action
290+
*/
291+
function confirmDeletePolicy() {
292+
if (policyToDelete) {
293+
fetch(`/api/v1/metrics-lifecycle-policies/${policyToDelete}`, {
294+
method: 'DELETE'
295+
})
296+
.then(response => {
297+
if (response.ok) {
298+
closeDeletePolicyModal();
299+
fetchAndDisplayPolicies();
300+
} else {
301+
return response.json().then(data => {
302+
throw new Error(data.message);
303+
});
304+
}
305+
})
306+
.catch(error => console.error('Error deleting policy:', error));
307+
}
308+
}

0 commit comments

Comments
 (0)