Skip to content

Commit

Permalink
Allow for showing all metrics on the reports overview page. Closes #7215
Browse files Browse the repository at this point in the history
.
  • Loading branch information
fniessink committed Oct 19, 2023
1 parent 7449ce7 commit 862fd23
Show file tree
Hide file tree
Showing 27 changed files with 301 additions and 329 deletions.
36 changes: 4 additions & 32 deletions components/api_server/src/routes/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import requests
from pymongo.database import Database

from shared.utils.functions import iso_timestamp
from shared.utils.type import ReportId
from shared_data_model import DATA_MODEL
from shared_data_model.parameters import PrivateToken
Expand Down Expand Up @@ -68,20 +67,12 @@ def get_report(database: Database, report_uuid: ReportId | None = None):
data_model = latest_datamodel(database, date_time)
reports = latest_reports_before_timestamp(database, data_model, date_time)
summarized_reports = []

if report_uuid and report_uuid.startswith("tag-"):
report = tag_report(data_model, report_uuid[4:], reports)
if len(report.subjects) > 0:
for report in reports:
if not report_uuid or report["report_uuid"] == report_uuid:
measurements = recent_measurements(database, report.metrics_dict, date_time)
summarized_reports.append(report.summarize(measurements))
else:
for report in reports:
if not report_uuid or report["report_uuid"] == report_uuid:
measurements = recent_measurements(database, report.metrics_dict, date_time)
summarized_reports.append(report.summarize(measurements))
else:
summarized_reports.append(report)

else:
summarized_reports.append(report)
hide_credentials(data_model, *summarized_reports)
return {"ok": True, "reports": summarized_reports}

Expand Down Expand Up @@ -246,22 +237,3 @@ def get_report_issue_tracker_options(database: Database, report: Report): # noq
"""Get options for the issue tracker attributes such as project key and issue type."""
issue_tracker = report.issue_tracker()
return issue_tracker.get_options().as_dict() | {"ok": True}


def tag_report(data_model, tag: str, reports: list[Report]) -> Report:
"""Create a report for a tag."""
subjects = {}
for report in reports:
for subject in report.subjects:
if tag_subject := subject.tag_subject(tag):
subjects[subject.uuid] = tag_subject

return Report(
data_model,
{
"title": f'Report for tag "{tag}"',
"report_uuid": f"tag-{tag}",
"timestamp": iso_timestamp(),
"subjects": subjects,
},
)
90 changes: 0 additions & 90 deletions components/api_server/tests/routes/test_report.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Unit tests for the report routes."""

from datetime import datetime, UTC
from typing import cast
from unittest.mock import Mock, patch
import copy

Expand Down Expand Up @@ -438,84 +436,6 @@ def test_issue_status(self):
report = get_report(self.database, REPORT_ID)["reports"][0]
self.assertEqual(issue_status, report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["issue_status"][0])

@patch("shared.utils.functions.datetime")
def test_get_tag_report(self, date_time):
"""Test that a tag report can be retrieved."""
date_time.now.return_value = now = datetime.now(tz=UTC)
self.database.reports.find_one.return_value = {
"_id": "id",
"report_uuid": REPORT_ID,
"title": "Report",
"subjects": {
"subject_without_metrics": {"metrics": {}},
SUBJECT_ID: {
"name": "Subject",
"type": "software",
"metrics": {
"metric_with_tag": {"type": "violations", "tags": ["tag"]},
"metric_without_tag": {"type": "violations", "tags": ["other tag"]},
},
},
},
}
expected_counts = {"blue": 0, "red": 0, "green": 0, "yellow": 0, "grey": 0, "white": 1}
self.assertDictEqual(
{
"ok": True,
"reports": [
{
"summary": expected_counts,
"title": 'Report for tag "tag"',
"report_uuid": "tag-tag",
"timestamp": now.replace(microsecond=0).isoformat(),
"subjects": {
SUBJECT_ID: {
"name": "Report ❯ Subject", # noqa: RUF001
"type": "software",
"metrics": {
"metric_with_tag": {
"status": None,
"status_start": None,
"scale": "count",
"sources": {},
"recent_measurements": [],
"latest_measurement": None,
"type": "violations",
"tags": ["tag"],
},
},
},
},
},
],
},
get_report(self.database, "tag-tag"),
)

@patch("shared.utils.functions.datetime")
def test_no_empty_tag_report(self, date_time):
"""Test that empty tag reports are omitted."""
date_time.now.return_value = datetime.now(tz=UTC)
self.database.reports.find.return_value = [
{
"_id": "id",
"report_uuid": REPORT_ID,
"title": "Report",
"subjects": {
"subject_without_metrics": {"metrics": {}},
SUBJECT_ID: {
"name": "Subject",
"type": "software",
"metrics": {
"metric_with_tag": {"type": "metric_type", "tags": ["tag"]},
"metric_without_tag": {"type": "metric_type", "tags": ["other tag"]},
},
},
},
},
]
self.assertDictEqual({"ok": True, "reports": []}, get_report(self.database, "tag-non-existing-tag"))

def test_add_report(self):
"""Test that a report can be added."""
self.assertTrue(post_report_new(self.database)["ok"])
Expand Down Expand Up @@ -564,16 +484,6 @@ def test_get_pdf_report(self, requests_get):
timeout=120,
)

@patch("requests.get")
def test_get_pdf_tag_report(self, requests_get):
"""Test that a PDF version of a tag report can be retrieved."""
requests_get.return_value = Mock(content=b"PDF")
self.assertEqual(b"PDF", export_report_as_pdf(cast(ReportId, "tag-security")))
requests_get.assert_called_once_with(
"http://renderer:9000/api/render?path=tag-security%3Fhide_toasts%3Dtrue",
timeout=120,
)

def test_delete_report(self):
"""Test that the report can be deleted."""
self.assertEqual({"ok": True}, delete_report(self.database, REPORT_ID))
Expand Down
9 changes: 1 addition & 8 deletions components/frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { get_report, get_reports_overview } from './api/report';
import { nr_measurements_api } from './api/measurement';
import { login } from './api/auth';
import { showMessage, showConnectionMessage } from './widgets/toast';
import { isValidDate_YYYYMMDD, registeredURLSearchParams, reportIsTagReport, toISODateStringInCurrentTZ } from './utils'
import { isValidDate_YYYYMMDD, registeredURLSearchParams, toISODateStringInCurrentTZ } from './utils'
import { AppUI } from './AppUI';
import 'react-toastify/dist/ReactToastify.css';
import './App.css';
Expand Down Expand Up @@ -132,13 +132,6 @@ class App extends Component {
open_report(event, report_uuid) {
event.preventDefault();
this.history_push(encodeURI(report_uuid))
if (reportIsTagReport(report_uuid)) {
showMessage(
"info",
"Tag reports are read-only",
"You opened a report for a specific metric tag. These reports are generated dynamically. Editing is not possible."
)
}
this.setState({ report_uuid: report_uuid, loading: true }, () => this.reload());
}

Expand Down
30 changes: 22 additions & 8 deletions components/frontend/src/AppUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { DataModel } from './context/DataModel';
import { DarkMode } from './context/DarkMode';
import { Permissions } from './context/Permissions';
import { PageContent } from './PageContent';
import { getReportsTags, getUserPermissions, reportIsTagReport, userPrefersDarkMode, useURLSearchQuery } from './utils'
import { getReportsTags, getUserPermissions, userPrefersDarkMode, useURLSearchQuery } from './utils'
import { datePropType, reportsPropType, stringsPropType } from './sharedPropTypes';

export function AppUI({
changed_fields,
Expand Down Expand Up @@ -47,18 +48,18 @@ export function AppUI({
return () => mediaQueryList.removeEventListener("change", changeMode);
}, [uiMode, setUIMode]);

const user_permissions = getUserPermissions(
user, email, reportIsTagReport(report_uuid), report_date, reports_overview.permissions || {}
)
const current_report = reports.filter((report) => report.report_uuid === report_uuid)[0] || null;
const user_permissions = getUserPermissions(user, email, report_date, reports_overview.permissions || {})
const atReportsOverview = report_uuid === ""
const current_report = atReportsOverview ? null : reports.filter((report) => report.report_uuid === report_uuid)[0];
const metricsToHideDefault = atReportsOverview ? "all" : "none"
// Make the settings changeable per report (and separately for the reports overview) by adding the report UUID as
// postfix to the settings key:
const urlSearchQueryKeyPostfix = report_uuid ? `_${report_uuid}` : ""
const [dateInterval, setDateInterval] = useURLSearchQuery("date_interval" + urlSearchQueryKeyPostfix, "integer", 7);
const [dateOrder, setDateOrder] = useURLSearchQuery("date_order" + urlSearchQueryKeyPostfix, "string", "descending");
const [hiddenColumns, toggleHiddenColumn, clearHiddenColumns] = useURLSearchQuery("hidden_columns" + urlSearchQueryKeyPostfix, "array");
const [hiddenTags, toggleHiddenTag, clearHiddenTags] = useURLSearchQuery("hidden_tags" + urlSearchQueryKeyPostfix, "array");
const [metricsToHide, setMetricsToHide] = useURLSearchQuery("metrics_to_hide" + urlSearchQueryKeyPostfix, "string", "none");
const [metricsToHide, setMetricsToHide] = useURLSearchQuery("metrics_to_hide" + urlSearchQueryKeyPostfix, "string", metricsToHideDefault);
const [nrDates, setNrDates] = useURLSearchQuery("nr_dates" + urlSearchQueryKeyPostfix, "integer", 1);
const [sortColumn, setSortColumn] = useURLSearchQuery("sort_column" + urlSearchQueryKeyPostfix, "string", null);
const [sortDirection, setSortDirection] = useURLSearchQuery("sort_direction" + urlSearchQueryKeyPostfix, "string", "ascending");
Expand Down Expand Up @@ -95,7 +96,6 @@ export function AppUI({

const darkMode = userPrefersDarkMode(uiMode);
const backgroundColor = darkMode ? "rgb(40, 40, 40)" : "white"
const atReportsOverview = report_uuid === ""
return (
<div style={{ display: "flex", minHeight: "100vh", flexDirection: "column", backgroundColor: backgroundColor }}>
<DarkMode.Provider value={darkMode}>
Expand Down Expand Up @@ -182,5 +182,19 @@ export function AppUI({
)
}
AppUI.propTypes = {
openReportsOverview: PropTypes.func
changed_fields: stringsPropType,
datamodel: PropTypes.object,
email: PropTypes.string,
handleDateChange: PropTypes.func,
last_update: datePropType,
loading: PropTypes.bool,
open_report: PropTypes.func,
openReportsOverview: PropTypes.func,
reload: PropTypes.func,
report_date: datePropType,
report_uuid: PropTypes.string,
reports: reportsPropType,
reports_overview: PropTypes.object,
set_user: PropTypes.func,
user: PropTypes.string
}
68 changes: 43 additions & 25 deletions components/frontend/src/PageContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Segment } from './semantic_ui_react_wrappers';
import { Report } from './report/Report';
import { ReportsOverview } from './report/ReportsOverview';
import { get_measurements } from './api/measurement';
import { metricsToHidePropType, stringsPropType } from './sharedPropTypes';
import { datePropType, issueSettingsPropType, metricsToHidePropType, reportsPropType, sortDirectionPropType, stringsPropType } from './sharedPropTypes';

function getColumnDates(reportDate, dateInterval, dateOrder, nrDates) {
const baseDate = reportDate ? new Date(reportDate) : new Date();
Expand All @@ -29,8 +29,8 @@ export function PageContent({
handleSort,
hiddenColumns,
hiddenTags,
metricsToHide,
issueSettings,
metricsToHide,
loading,
nrDates,
nrMeasurements,
Expand Down Expand Up @@ -60,45 +60,63 @@ export function PageContent({
if (loading) {
content = <Segment basic placeholder aria-label="Loading..."><Loader active size="massive" /></Segment>
} else {
const commonProps = {
changed_fields: changed_fields,
dates: dates,
handleSort: handleSort,
hiddenColumns: hiddenColumns,
hiddenTags: hiddenTags,
issueSettings: issueSettings,
measurements: measurements,
metricsToHide: metricsToHide,
reload: reload,
reports: reports,
report_date: report_date,
sortColumn: sortColumn,
sortDirection: sortDirection,
toggleHiddenTag: toggleHiddenTag,
toggleVisibleDetailsTab: toggleVisibleDetailsTab,
visibleDetailsTabs: visibleDetailsTabs
}
if (report_uuid) {
content = <Report
changed_fields={changed_fields}
dates={dates}
openReportsOverview={openReportsOverview}
handleSort={handleSort}
hiddenColumns={hiddenColumns}
hiddenTags={hiddenTags}
metricsToHide={metricsToHide}
issueSettings={issueSettings}
measurements={measurements}
reload={reload}
report={current_report}
reports={reports}
report_date={report_date}
sortColumn={sortColumn}
sortDirection={sortDirection}
toggleHiddenTag={toggleHiddenTag}
toggleVisibleDetailsTab={toggleVisibleDetailsTab}
visibleDetailsTabs={visibleDetailsTabs}
{...commonProps}
/>
} else {
content = <ReportsOverview
dates={dates}
hiddenTags={hiddenTags}
measurements={measurements}
open_report={open_report}
reload={reload}
reports={reports}
reports_overview={reports_overview}
report_date={report_date}
{...commonProps}
/>
}
}
return <Container fluid className="MainContainer">{content}</Container>
}
PageContent.propTypes = {
changed_fields: stringsPropType,
current_report: PropTypes.object,
dateInterval: PropTypes.number,
dateOrder: sortDirectionPropType,
handleSort: PropTypes.func,
hiddenColumns: stringsPropType,
hiddenTags: stringsPropType,
issueSettings: issueSettingsPropType,
metricsToHide: metricsToHidePropType,
loading: PropTypes.bool,
nrDates: PropTypes.number,
nrMeasurements: PropTypes.number,
open_report: PropTypes.func,
openReportsOverview: PropTypes.func,
toggleHiddenTag: PropTypes.func
reload: PropTypes.func,
report_date: datePropType,
report_uuid: PropTypes.string,
reports: reportsPropType,
reports_overview: PropTypes.object,
sortColumn: PropTypes.string,
sortDirection: sortDirectionPropType,
toggleHiddenTag: PropTypes.func,
toggleVisibleDetailsTab: PropTypes.func,
visibleDetailsTabs: stringsPropType
}
3 changes: 2 additions & 1 deletion components/frontend/src/__fixtures__/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ export const report = {
}
}
}
}
},
title: "Report title"
}
4 changes: 2 additions & 2 deletions components/frontend/src/header_footer/Menubar.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import FocusLock from 'react-focus-lock';
import { login, logout } from '../api/auth';
import { Avatar } from '../widgets/Avatar';
import { DatePicker } from '../widgets/DatePicker';
import { datePropType, uiModePropType } from '../sharedPropTypes';
import { datePropType, stringsPropType, uiModePropType } from '../sharedPropTypes';
import { UIModeMenu } from './UIModeMenu';
import './Menubar.css';

Expand Down Expand Up @@ -178,5 +178,5 @@ Menubar.propTypes = {
setUIMode: PropTypes.func,
uiMode: uiModePropType,
user: PropTypes.string,
visibleDetailsTabs: PropTypes.array
visibleDetailsTabs: stringsPropType
}
Loading

0 comments on commit 862fd23

Please sign in to comment.