From 862fd234fb9a3fd1715db3486e2865918f130296 Mon Sep 17 00:00:00 2001 From: Frank Niessink Date: Tue, 17 Oct 2023 11:03:17 +0200 Subject: [PATCH] Allow for showing all metrics on the reports overview page. Closes #7215. --- components/api_server/src/routes/report.py | 36 +------- .../api_server/tests/routes/test_report.py | 90 ------------------- components/frontend/src/App.js | 9 +- components/frontend/src/AppUI.js | 30 +++++-- components/frontend/src/PageContent.js | 68 ++++++++------ .../frontend/src/__fixtures__/fixtures.js | 3 +- .../frontend/src/header_footer/Menubar.js | 4 +- .../src/header_footer/SettingsPanel.js | 5 +- .../src/header_footer/SettingsPanel.test.js | 2 +- components/frontend/src/report/Report.js | 7 +- .../frontend/src/report/ReportsOverview.js | 85 +++++++++++++++--- .../src/report/ReportsOverview.test.js | 71 ++++++++++++--- components/frontend/src/sharedPropTypes.js | 18 ++-- components/frontend/src/subject/Subject.js | 9 +- .../frontend/src/subject/Subject.test.js | 16 ++-- .../frontend/src/subject/SubjectTable.js | 29 ++++-- .../frontend/src/subject/SubjectTitle.js | 10 ++- components/frontend/src/subject/Subjects.js | 10 ++- .../frontend/src/subject/SubjectsButtonRow.js | 5 +- components/frontend/src/utils.js | 8 +- components/frontend/src/utils.test.js | 22 ++--- .../shared_code/src/shared/model/report.py | 14 ++- .../shared_code/src/shared/model/subject.py | 12 +-- .../tests/shared/model/test_subject.py | 24 ----- docs/src/changelog.md | 1 + .../src/features/tagreport.feature | 17 ---- tests/feature_tests/src/steps/tagreport.py | 25 ------ 27 files changed, 301 insertions(+), 329 deletions(-) delete mode 100644 tests/feature_tests/src/features/tagreport.feature delete mode 100644 tests/feature_tests/src/steps/tagreport.py diff --git a/components/api_server/src/routes/report.py b/components/api_server/src/routes/report.py index 5ea21fdb2b..52591feb32 100644 --- a/components/api_server/src/routes/report.py +++ b/components/api_server/src/routes/report.py @@ -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 @@ -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} @@ -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, - }, - ) diff --git a/components/api_server/tests/routes/test_report.py b/components/api_server/tests/routes/test_report.py index f99270e446..5db08d6ed3 100644 --- a/components/api_server/tests/routes/test_report.py +++ b/components/api_server/tests/routes/test_report.py @@ -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 @@ -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"]) @@ -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)) diff --git a/components/frontend/src/App.js b/components/frontend/src/App.js index 44d1b268a5..b43a7e2598 100644 --- a/components/frontend/src/App.js +++ b/components/frontend/src/App.js @@ -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'; @@ -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()); } diff --git a/components/frontend/src/AppUI.js b/components/frontend/src/AppUI.js index 1d94ae55a3..b7c6fbca70 100644 --- a/components/frontend/src/AppUI.js +++ b/components/frontend/src/AppUI.js @@ -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, @@ -47,10 +48,10 @@ 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}` : "" @@ -58,7 +59,7 @@ export function AppUI({ 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"); @@ -95,7 +96,6 @@ export function AppUI({ const darkMode = userPrefersDarkMode(uiMode); const backgroundColor = darkMode ? "rgb(40, 40, 40)" : "white" - const atReportsOverview = report_uuid === "" return (
@@ -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 } \ No newline at end of file diff --git a/components/frontend/src/PageContent.js b/components/frontend/src/PageContent.js index 83b111013d..7179ddca6f 100644 --- a/components/frontend/src/PageContent.js +++ b/components/frontend/src/PageContent.js @@ -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(); @@ -29,8 +29,8 @@ export function PageContent({ handleSort, hiddenColumns, hiddenTags, - metricsToHide, issueSettings, + metricsToHide, loading, nrDates, nrMeasurements, @@ -60,45 +60,63 @@ export function PageContent({ if (loading) { content = } 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 = } else { content = } } return {content} } 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 } diff --git a/components/frontend/src/__fixtures__/fixtures.js b/components/frontend/src/__fixtures__/fixtures.js index 25b1f6b78c..dab539a18f 100644 --- a/components/frontend/src/__fixtures__/fixtures.js +++ b/components/frontend/src/__fixtures__/fixtures.js @@ -44,5 +44,6 @@ export const report = { } } } - } + }, + title: "Report title" } diff --git a/components/frontend/src/header_footer/Menubar.js b/components/frontend/src/header_footer/Menubar.js index b2cfe8b406..013f37be6d 100644 --- a/components/frontend/src/header_footer/Menubar.js +++ b/components/frontend/src/header_footer/Menubar.js @@ -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'; @@ -178,5 +178,5 @@ Menubar.propTypes = { setUIMode: PropTypes.func, uiMode: uiModePropType, user: PropTypes.string, - visibleDetailsTabs: PropTypes.array + visibleDetailsTabs: stringsPropType } diff --git a/components/frontend/src/header_footer/SettingsPanel.js b/components/frontend/src/header_footer/SettingsPanel.js index 9d0257e62a..a7c1de8e36 100644 --- a/components/frontend/src/header_footer/SettingsPanel.js +++ b/components/frontend/src/header_footer/SettingsPanel.js @@ -494,6 +494,7 @@ function ResetSettingsButton( visibleDetailsTabs } ) { + const metricsToHideDefault = atReportsOverview ? "all" : "none" return (