Merge branch 'main' of https://github.com/GSA/notifications-admin into JB-favicon-fix

This commit is contained in:
Jonathan Bobel
2024-08-15 12:30:02 -04:00
6 changed files with 94 additions and 63 deletions

View File

@@ -26,6 +26,13 @@
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
let tooltip = d3.select('#tooltip');
if (tooltip.empty()) {
tooltip = d3.select('body').append('div')
.attr('id', 'tooltip')
.style('display', 'none');
}
// Create legend
const legendContainer = d3.select('.chart-legend');
legendContainer.selectAll('*').remove(); // Clear any existing legend
@@ -95,10 +102,6 @@
const color = d3.scaleOrdinal()
.domain(['delivered', 'failed'])
.range([COLORS.delivered, COLORS.failed]);
// Create tooltip
const tooltip = d3.select('body').append('div')
.attr('id', 'tooltip')
.style('display', 'none');
// Create bars with animation
const barGroups = svg.selectAll('.bar-group')
@@ -185,41 +188,35 @@
return;
}
var daily_stats = activityChartContainer.getAttribute('data-daily-stats');
var daily_stats_by_user = activityChartContainer.getAttribute('data-daily_stats_by_user');
var url = type === 'service' ? `/daily_stats.json` : `/daily_stats_by_user.json`;
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
labels = [];
deliveredData = [];
failedData = [];
try {
// Choose the correct JSON string based on the type ('service' or 'user'),
// replace single quotes with double quotes to ensure valid JSON format,
// then parse the JSON string into a JavaScript object.
var statsJson = type === 'service' ? daily_stats : daily_stats_by_user;
statsJson = statsJson.replace(/'/g, '"');
data = JSON.parse(statsJson);
} catch (error) {
console.error('Error parsing JSON data:', error);
return;
}
var labels = [];
var deliveredData = [];
var failedData = [];
for (var dateString in data) {
if (data.hasOwnProperty(dateString)) {
const dateParts = dateString.split('-');
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
for (var dateString in data) {
if (data.hasOwnProperty(dateString)) {
const dateParts = dateString.split('-');
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered);
failedData.push(data[dateString].sms.failure);
}
}
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered);
failedData.push(data[dateString].sms.failure);
}
}
try {
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
} catch (error) {
console.error('Error creating chart or table:', error);
}
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
return data;
})
.catch(error => console.error('Error fetching daily stats:', error));
};
const handleDropdownChange = function(event) {
@@ -228,13 +225,8 @@
const selectElement = document.getElementById('options');
const selectedText = selectElement.options[selectElement.selectedIndex].text;
if (selectedValue === "individual") {
subTitle.textContent = selectedText + " - last 7 days";
fetchData('individual');
} else if (selectedValue === "service") {
subTitle.textContent = selectedText + " - last 7 days";
fetchData('service');
}
subTitle.textContent = `${selectedText} - last 7 days`;
fetchData(selectedValue);
// Update ARIA live region
const liveRegion = document.getElementById('aria-live-account');
@@ -263,8 +255,10 @@
// Resize chart on window resize
window.addEventListener('resize', function() {
const selectedValue = document.getElementById('options').value;
handleDropdownChange({ target: { value: selectedValue } });
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0) {
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
}
});
// Export functions for testing

View File

@@ -53,17 +53,12 @@ def service_dashboard(service_id):
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
current_service.id,
)
date_range = get_stats_date_range()
usage_data = get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
sms_sent = usage_data["sms_sent"]
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
job_response = job_api_client.get_jobs(service_id)["data"]
service_data_retention_days = 7
daily_stats = get_daily_stats(service_id, date_range)
daily_stats_by_user = get_daily_stats_by_user(
service_id, current_user.id, date_range
)
jobs = [
{
@@ -94,24 +89,32 @@ def service_dashboard(service_id):
service_data_retention_days=service_data_retention_days,
sms_sent=sms_sent,
sms_allowance_remaining=sms_allowance_remaining,
daily_stats=daily_stats,
daily_stats_by_user=daily_stats_by_user,
)
def get_daily_stats(service_id, date_range):
return service_api_client.get_service_notification_statistics_by_day(
@main.route("/daily_stats.json")
def get_daily_stats():
service_id = session.get("service_id")
date_range = get_stats_date_range()
stats = service_api_client.get_service_notification_statistics_by_day(
service_id, start_date=date_range["start_date"], days=date_range["days"]
)
return jsonify(stats)
def get_daily_stats_by_user(service_id, user_id, date_range):
return service_api_client.get_user_service_notification_statistics_by_day(
@main.route("/daily_stats_by_user.json")
def get_daily_stats_by_user():
service_id = session.get("service_id")
date_range = get_stats_date_range()
user_id = current_user.id
stats = service_api_client.get_user_service_notification_statistics_by_day(
service_id,
user_id,
start_date=date_range["start_date"],
days=date_range["days"],
)
return jsonify(stats)
@main.route("/services/<uuid:service_id>/dashboard.json")

View File

@@ -32,7 +32,7 @@
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">
Activity snapshot
</h2>
<div id="activityChartContainer" data-daily-stats="{{ daily_stats }}" data-daily_stats_by_user="{{ daily_stats_by_user }}">
<div id="activityChartContainer">
<form class="usa-form">
<label class="usa-label" for="options">Account</label>
<select class="usa-select margin-bottom-2" name="options" id="options">

View File

@@ -21,7 +21,10 @@ MOCK_JOBS = {
"scheduled_for": None,
"service": "21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3",
"service_name": {"name": "Mock Texting Service"},
"statistics": [{"count": 1, "status": "delivered"}, {"count": 5, "status": "failed"}],
"statistics": [
{"count": 1, "status": "delivered"},
{"count": 5, "status": "failed"},
],
"template": "6a456418-498c-4c86-b0cd-9403c14a216c",
"template_name": "Mock Template Name",
"template_type": "sms",
@@ -63,7 +66,15 @@ def test_all_activity(
assert table is not None, "Table not found in the response"
headers = [th.get_text(strip=True) for th in table.find_all("th")]
expected_headers = ["Job ID#", "Template", "Time sent", "Sender", "Report", "Delivered", "Failed"]
expected_headers = [
"Job ID#",
"Template",
"Time sent",
"Sender",
"Report",
"Delivered",
"Failed",
]
assert (
headers == expected_headers
@@ -103,9 +114,7 @@ def test_all_activity(
), f"Expected delivered count '1', but got '{delivered_cell}'"
failed_cell = cells[6].get_text(strip=True)
assert (
failed_cell == "5"
), f"Expected failed count '5', but got '{failed_cell}'"
assert failed_cell == "5", f"Expected failed count '5', but got '{failed_cell}'"
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)

View File

@@ -96,6 +96,8 @@ EXCLUDED_ENDPOINTS = tuple(
"get_users_report",
"get_daily_volumes",
"get_daily_sms_provider_volumes",
"get_daily_stats",
"get_daily_stats_by_user",
"get_volumes_by_service",
"get_example_csv",
"get_notifications_as_json",

View File

@@ -21,7 +21,7 @@ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
beforeAll(done => {
// Set up the DOM with the D3 script included
document.body.innerHTML = `
<div id="activityChartContainer" data-daily-stats="{{ daily_stats }}" data-daily_stats_by_user="{{ daily_stats_by_user }}">
<div id="activityChartContainer"">
<form class="usa-form">
<label class="usa-label" for="options">Account</label>
<select class="usa-select margin-bottom-2" name="options" id="options">
@@ -124,3 +124,26 @@ test('Check HTML content after chart creation', () => {
expect(container.querySelector('svg')).not.toBeNull();
expect(container.querySelectorAll('rect').length).toBeGreaterThan(0);
});
test('Fetches data and creates chart and table correctly', async () => {
const mockResponse = {
'2024-07-01': { sms: { delivered: 50, failed: 5 } },
'2024-07-02': { sms: { delivered: 60, failed: 2 } },
'2024-07-03': { sms: { delivered: 70, failed: 1 } },
'2024-07-04': { sms: { delivered: 80, failed: 0 } },
'2024-07-05': { sms: { delivered: 90, failed: 3 } },
'2024-07-06': { sms: { delivered: 100, failed: 4 } },
'2024-07-07': { sms: { delivered: 110, failed: 2 } },
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockResponse),
})
);
const data = await fetchData('service');
expect(global.fetch).toHaveBeenCalledWith('/daily_stats.json');
expect(data).toEqual(mockResponse);
});