Merge pull request #2812 from GSA/2802-making-the-data-more-scannable-through-better-formatting-and-status-indicators

cleaning up css and add better visual for pagination
This commit is contained in:
Beverly Nguyen
2025-08-07 14:34:13 -07:00
committed by GitHub
11 changed files with 92 additions and 96 deletions

View File

@@ -295,24 +295,28 @@
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
const tableHeading = document.querySelector('#tableActivity h2');
const senderColumns = document.querySelectorAll('.sender-column');
const senderElements = [
document.querySelector('[data-column="sender"]'),
...document.querySelectorAll('[data-sender]')
];
const allRows = document.querySelectorAll('#activity-table tbody tr');
const caption = document.querySelector('#activity-table caption');
const table = document.getElementById('activity-table');
const caption = table.querySelector('caption');
if (selectedValue === 'individual') {
tableHeading.textContent = 'My activity';
caption.textContent = `Table showing the sent jobs for ${currentUserName}`;
senderColumns.forEach(col => {
col.style.display = 'none';
senderElements.forEach(el => {
if (el) el.style.display = 'none';
});
allRows.forEach(row => row.style.display = 'none');
const userRows = Array.from(allRows).filter(row => {
const senderCell = row.querySelector('.sender-column');
const rowSender = senderCell ? senderCell.textContent.trim() : '';
const senderCell = row.querySelector('[data-sender]');
const rowSender = senderCell ? senderCell.dataset.sender : '';
return rowSender === currentUserName;
});
@@ -333,8 +337,8 @@
tableHeading.textContent = 'Service activity';
caption.textContent = `Table showing the sent jobs for service`;
senderColumns.forEach(col => {
col.style.display = '';
senderElements.forEach(el => {
if (el) el.style.display = '';
});
allRows.forEach((row, index) => {

View File

@@ -492,44 +492,12 @@ td.table-empty-message {
.job-table {
width: 100%;
border-collapse: collapse;
td.file-name {
width: 25%;
overflow-wrap: anywhere;
}
td.jobid {
width: 5%;
}
td.template {
width: 25%;
}
td.time-sent {
width: 15%;
}
td.sender {
width: 20%;
overflow-wrap: break-word;
}
td.count-of-recipients {
width: 5%;
}
td.report {
width: 2%;
text-align: center;
}
td.delivered {
width: 2%;
text-align: center;
}
td.failed {
width: 2%;
text-align: center;
}
td.report img {
padding-top: 5px;
}
th {
padding: 0.5rem 1rem;
}
td {
padding: 0.5rem 1rem;
}
}
@media (max-width: 768px) {

View File

@@ -56,6 +56,7 @@ def all_jobs_activity(service_id):
next_page=next_page,
prev_page=prev_page,
pagination=pagination,
total_jobs=jobs.get("total", 0),
**download_availability,
download_link_one_day=url_for(
".download_notifications_csv",

View File

@@ -51,6 +51,11 @@
{% endif %}
</ul>
</nav>
{% if pagination and total_jobs %}
<p class="text-center font-body-sm">
Page <span class="text-bold">{{ pagination.current }}</span> of <span class="text-bold">{{ pagination.last }}</span> (<span>{{ total_jobs }}</span> total jobs)
</p>
{% endif %}
{% endif %}
{% endset %}
{% block maincolumn_content %}
@@ -58,7 +63,7 @@
<h1 class="usa-sr-only">All activity</h1>
<h2 class="font-body-2xl line-height-sans-2 margin-0">All activity</h2>
<h2 class="margin-top-4 margin-bottom-1">Sent jobs</h2>
<div class="usa-table-container--scrollable-mobile">
<div class="usa-table-container--scrollable-mobile table-overflow-x-auto">
<table class="usa-table usa-table--compact job-table">
<caption class="usa-sr-only">Table showing all sent jobs for this service</caption>
<thead class="table-field-headings">
@@ -66,23 +71,20 @@
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId">
<span>Job ID#</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<th scope="col" role="columnheader" class="table-field-heading">
<span>Template</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<th scope="col" role="columnheader" class="table-field-heading">
<span>Started</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<th scope="col" role="columnheader" class="table-field-heading">
<span>Sender</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<th scope="col" role="columnheader" class="table-field-heading">
<span>Report</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<span>Delivered</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<span>Failed</span>
<th scope="col" role="columnheader" class="table-field-heading">
<span>Status</span>
</th>
</tr>
</thead>
@@ -90,28 +92,34 @@
{% if all_jobs_dict %}
{% for job in all_jobs_dict %}
<tr class="table-row">
<td class="table-field jobid" role="rowheader">
<td class="table-field width-5" role="rowheader">
<a class="usa-link" href="{{ job.view_job_link }}">
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
</a>
</td>
<td class="table-field template">{{ job.template_name }}</td>
<td data-sort-value="{{ job.activity_time | convert_time_unixtimestamp }}" class="table-field time-sent">
<td class="table-field width-16">{{ job.template_name }}</td>
<td data-sort-value="{{ job.activity_time | convert_time_unixtimestamp }}" class="table-field width-18">
{{ job.activity_time|format_datetime_table }}
</td>
<td class="table-field sender">{{ job.created_by.name }}</td>
<td class="table-field report">
<td class="table-field width-30 ">{{ job.created_by.name }}</td>
<td class="text-center table-field width-2">
{% if job.can_download %}
<a href="{{ job.download_link }}">
<img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="">
<img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="" class="padding-top-05">
<span class="usa-sr-only">Download report link</span>
</a>
{% else %}
<span>N/A</span>
{% endif %}
</td>
<td class="table-field delivered">{{ job.delivered_count if job.delivered_count is not none else '0' }}</td>
<td class="table-field failed">{{ job.failed_count if job.failed_count is not none else '0' }}</td>
<td class="table-field width-15">
<span class="bg-base-lighter padding-x-05 padding-y-0 font-body-3xs text-bold radius-sm margin-right-05 display-inline-block text-nowrap">
{{ job.delivered_count if job.delivered_count is not none else '0' }} delivered
</span>
<span class="bg-base-lighter padding-x-05 padding-y-0 font-body-3xs text-bold radius-sm display-inline-block text-nowrap">
{{ job.failed_count if job.failed_count is not none else '0' }} failed
</span>
</td>
</tr>
{% endfor %}
{% else %}
@@ -122,7 +130,7 @@
</tbody>
</table>
<div class="usa-sr-only usa-table__announcement-region" aria-live="polite"></div>
<p><b>Note: </b>Report data is only available for 7 days after your message has been sent</p>
<p class="font-body-sm"><b>Note: </b>Report data is only available for 7 days after your message has been sent</p>
</div>
{{show_pagination}}
{% if current_user.has_permissions(ServicePermission.VIEW_ACTIVITY) %}

View File

@@ -22,15 +22,15 @@
<div id="tableActivity" class="table-overflow-x-auto">
<h2 id="table-heading" class="margin-top-4 margin-bottom-1">Service activity</h2>
<table class="usa-table job-table" id="activity-table">
<table class="usa-table usa-table--compact job-table" id="activity-table">
<caption class="usa-sr-only">Table showing the sent jobs for {{current_service.name}}</caption>
<thead class="table-field-headings">
<tr>
<th scope="col" class="table-field-heading-first" id="jobId">Job ID#</th>
<th data-sortable scope="col" class="table-field-heading" scope="col">Template</th>
<th data-sortable scope="col" class="table-field-heading">Job status</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading sender-column">Sender
<th data-sortable scope="col" class="table-field-heading width-30">Job status</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading" data-column="sender">Sender
</th>
<th data-sortable scope="col" class="table-field-heading"># of Recipients</th>
</tr>
@@ -39,13 +39,13 @@
{% if jobs %}
{% for job in jobs %}
<tr id="{{ job.id }}">
<td class="table-field jobid" role="rowheader">
<td class="table-field" role="rowheader">
<a class="usa-link" href="{{ url_for('.view_job', service_id=current_service.id, job_id=job.id )}}">
{{ job.id[:8] if job.id else 'Manually entered number' }}
</a>
</td>
<td class="table-field template">{{ job.template_name }}</td>
<td class="table-field time-sent">
<td class="table-field width-35">{{ job.template_name }}</td>
<td class="table-field width-20">
{% if not job.finished_processing %}
{% if job.scheduled_for%}
Scheduled for {{ job.scheduled_for|format_datetime_table }}
@@ -58,8 +58,8 @@
Sent on {{ job.processing_started|format_datetime_table }}
{% endif %}
</td>
<td class="table-field sender sender-column">{{ job.created_by.name }}</td>
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
<td class="table-field" data-sender="{{ job.created_by.name }}">{{ job.created_by.name }}</td>
<td class="table-field width-5">{{ job.notification_count }}</td>
</tr>
{% endfor %}
{% else %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

View File

@@ -2,6 +2,7 @@ import multiprocessing
import os
import sys
import traceback
import gunicorn
# Let gunicorn figure out the right number of workers

View File

@@ -1,7 +1,6 @@
import json
import logging
import urllib.parse
from os import getenv
import requests

View File

@@ -77,8 +77,7 @@ def test_all_activity(
"Started",
"Sender",
"Report",
"Delivered",
"Failed",
"Status",
]
assert (
@@ -90,7 +89,7 @@ def test_all_activity(
job_row = rows[0]
cells = job_row.find_all("td")
assert len(cells) == 7, "Expected five columns in the job row"
assert len(cells) == 6, "Expected six columns in the job row"
job_id_cell = cells[0].find("a").get_text(strip=True)
@@ -113,13 +112,9 @@ def test_all_activity(
report_cell = cells[4].find("span").get_text(strip=True)
assert report_cell == "N/A", f"Expected report 'N/A', but got '{report_cell}'"
delivered_cell = cells[5].get_text(strip=True)
assert (
delivered_cell == "1"
), 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}'"
status_cell = cells[5].get_text(strip=True)
assert "1 delivered" in status_cell, f"Expected status to contain '1 delivered', but got '{status_cell}'"
assert "5 failed" in status_cell, f"Expected status to contain '5 failed', but got '{status_cell}'"
def test_all_activity_no_jobs(client_request, mocker):

View File

@@ -239,14 +239,19 @@ test('handleDropdownChange updates DOM for individual selection', () => {
<h2 id="table-heading"></h2>
<table id="activity-table">
<caption id="caption"></caption>
<thead>
<tr>
<th data-column="sender">Sender</th>
</tr>
</thead>
<tbody>
<tr><td class="sender-column">Test User</td></tr>
<tr><td class="sender-column">Other User</td></tr>
<tr><td class="sender-column">Test User</td></tr>
<tr><td class="sender-column">Test User</td></tr>
<tr><td class="sender-column">Other User</td></tr>
<tr><td class="sender-column">Test User</td></tr>
<tr><td class="sender-column">Test User</td></tr>
<tr><td data-sender="Test User">Test User</td></tr>
<tr><td data-sender="Other User">Other User</td></tr>
<tr><td data-sender="Test User">Test User</td></tr>
<tr><td data-sender="Test User">Test User</td></tr>
<tr><td data-sender="Other User">Other User</td></tr>
<tr><td data-sender="Test User">Test User</td></tr>
<tr><td data-sender="Test User">Test User</td></tr>
</tbody>
</table>
</div>
@@ -269,15 +274,18 @@ test('handleDropdownChange updates DOM for individual selection', () => {
expect(document.getElementById('table-heading').textContent).toBe('My activity');
expect(document.getElementById('caption').textContent).toContain('Test User');
document.querySelectorAll('.sender-column').forEach(col => {
expect(col.style.display).toBe('none');
const senderHeader = document.querySelector('[data-column="sender"]');
expect(senderHeader.style.display).toBe('none');
document.querySelectorAll('[data-sender]').forEach(cell => {
expect(cell.style.display).toBe('none');
});
const rows = Array.from(document.querySelectorAll('#activity-table tbody tr'));
const visibleRows = rows.filter(row => row.style.display !== 'none');
expect(visibleRows.length).toBeLessThanOrEqual(5);
visibleRows.forEach(row => {
const sender = row.querySelector('.sender-column').textContent.trim();
const sender = row.querySelector('[data-sender]').dataset.sender;
expect(sender).toBe('Test User');
});
@@ -295,10 +303,15 @@ test('handleDropdownChange shows empty message when user has no jobs', () => {
<h2 id="table-heading"></h2>
<table id="activity-table">
<caption id="caption"></caption>
<thead>
<tr>
<th data-column="sender">Sender</th>
</tr>
</thead>
<tbody>
<tr><td class="sender-column">Other User</td></tr>
<tr><td class="sender-column">Another User</td></tr>
<tr><td class="sender-column">Different User</td></tr>
<tr><td data-sender="Other User">Other User</td></tr>
<tr><td data-sender="Another User">Another User</td></tr>
<tr><td data-sender="Different User">Different User</td></tr>
<tr class="table-row">
<td class="table-empty-message" colspan="10">No batched job messages found (messages are kept for 7 days).</td>
</tr>
@@ -324,8 +337,11 @@ test('handleDropdownChange shows empty message when user has no jobs', () => {
expect(document.getElementById('table-heading').textContent).toBe('My activity');
expect(document.getElementById('caption').textContent).toContain('Test User');
document.querySelectorAll('.sender-column').forEach(col => {
expect(col.style.display).toBe('none');
const senderHeader = document.querySelector('[data-column="sender"]');
expect(senderHeader.style.display).toBe('none');
document.querySelectorAll('[data-sender]').forEach(cell => {
expect(cell.style.display).toBe('none');
});
const emptyMessageRow = document.querySelector('.table-empty-message').closest('tr');

View File

@@ -67,6 +67,10 @@ const routes = {
label: 'Team Members',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users',
},
{
label: 'All Activity',
path: '/activity/services/9c765540-266e-474e-b6bb-8e2e0e32b781',
},
{
label: 'Invite User',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users/invite',