diff --git a/components/frontend/src/metric/MetricDetails.js b/components/frontend/src/metric/MetricDetails.js index e04d72baf0..edf3576c4e 100644 --- a/components/frontend/src/metric/MetricDetails.js +++ b/components/frontend/src/metric/MetricDetails.js @@ -128,6 +128,7 @@ export function MetricDetails({ const metric = subject.metrics[metric_uuid] const lastMeasurement = measurements[measurements.length - 1] let anyError = lastMeasurement?.sources.some((source) => source.connection_error || source.parse_error) + let anyWarning = Object.values(metric.sources).some((source) => dataModel.sources[source.type].deprecated) anyError = anyError || Object.values(metric.sources ?? {}).some( @@ -156,7 +157,7 @@ export function MetricDetails({ changed_fields={changed_fields} reload={reload} />, - { iconName: "server", error: Boolean(anyError) }, + { iconName: "server", error: Boolean(anyError), warning: Boolean(anyWarning) }, ), tabPane( "Technical debt", diff --git a/components/frontend/src/metric/MetricDetails.test.js b/components/frontend/src/metric/MetricDetails.test.js index bf5f01f85f..7c50e499f0 100644 --- a/components/frontend/src/metric/MetricDetails.test.js +++ b/components/frontend/src/metric/MetricDetails.test.js @@ -43,6 +43,7 @@ const dataModel = { sources: { source_type: { name: "The source", + deprecated: true, parameters: {}, parameter_layout: { all: { @@ -166,6 +167,11 @@ it("displays whether sources have errors", async () => { expect(screen.getByText(/Sources/)).toHaveClass("red label") }) +it("displays whether sources have warnings", async () => { + await renderMetricDetails() + expect(screen.getByText(/Sources/)).toHaveClass("yellow label") +}) + it("moves the metric", async () => { const mockCallback = jest.fn() await renderMetricDetails(mockCallback) diff --git a/components/frontend/src/source/SourceType.js b/components/frontend/src/source/SourceType.js index 03ecfca332..6e6935afc4 100644 --- a/components/frontend/src/source/SourceType.js +++ b/components/frontend/src/source/SourceType.js @@ -4,7 +4,7 @@ import { useContext } from "react" import { DataModel } from "../context/DataModel" import { EDIT_REPORT_PERMISSION } from "../context/Permissions" import { SingleChoiceInput } from "../fields/SingleChoiceInput" -import { Header } from "../semantic_ui_react_wrappers" +import { Header, Label } from "../semantic_ui_react_wrappers" import { dataModelPropType, sourceTypePropType } from "../sharedPropTypes" import { Logo } from "./Logo" @@ -29,6 +29,7 @@ function sourceTypeOption(key, sourceType) { {sourceType.name} + {sourceType.deprecated && } {sourceTypeDescription(sourceType)} diff --git a/components/frontend/src/source/SourceType.test.js b/components/frontend/src/source/SourceType.test.js index 3f217c9c9b..7431c328f2 100644 --- a/components/frontend/src/source/SourceType.test.js +++ b/components/frontend/src/source/SourceType.test.js @@ -25,6 +25,7 @@ const dataModel = { }, gitlab: { name: "GitLab", + deprecated: true, }, unsupported: { name: "Unsupported", @@ -68,3 +69,10 @@ it("shows the supported source versions", async () => { }) expect(screen.queryAllByText(/Supported SonarQube versions: >=8.2/).length).toBe(1) }) + +it("shows sources as deprecated if they are deprecated", async () => { + await act(async () => { + renderSourceType("violations", "sonarqube") + }) + expect(screen.getAllByText(/Deprecated/).length).toBe(1) +}) diff --git a/components/frontend/src/source/SourceTypeHeader.js b/components/frontend/src/source/SourceTypeHeader.js index 19f848db1f..cf05d2071a 100644 --- a/components/frontend/src/source/SourceTypeHeader.js +++ b/components/frontend/src/source/SourceTypeHeader.js @@ -1,6 +1,6 @@ import { string } from "prop-types" -import { Header, Icon } from "../semantic_ui_react_wrappers" +import { Header, Icon, Label } from "../semantic_ui_react_wrappers" import { sourceTypePropType } from "../sharedPropTypes" import { slugify } from "../utils" import { HyperLink } from "../widgets/HyperLink" @@ -18,6 +18,7 @@ export function SourceTypeHeader({ metricTypeId, sourceTypeId, sourceType }) { {sourceType.name} + {sourceType.deprecated && } {`${sourceTypeDescription(sourceType)} `} diff --git a/components/frontend/src/source/SourceTypeHeader.test.js b/components/frontend/src/source/SourceTypeHeader.test.js index 960a2c070c..5526305e60 100644 --- a/components/frontend/src/source/SourceTypeHeader.test.js +++ b/components/frontend/src/source/SourceTypeHeader.test.js @@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react" import { SourceTypeHeader } from "./SourceTypeHeader" -function renderSourceTypeHeader(documentation, metricTypeId) { +function renderSourceTypeHeader(documentation, metricTypeId, deprecated) { render( =1.0", + deprecated: deprecated, }} />, ) @@ -42,3 +43,13 @@ it("shows the supported source versions", () => { renderSourceTypeHeader() expect(screen.getAllByText(/Supported Source type versions: >=1.0/).length).toBe(1) }) + +it("does not show the source as deprecated if it is not deprecated", () => { + renderSourceTypeHeader() + expect(screen.queryAllByText(/Deprecated/).length).toBe(0) +}) + +it("shows the source as deprecated if it is deprecated", () => { + renderSourceTypeHeader({}, null, true) + expect(screen.getAllByText(/Deprecated/).length).toBe(1) +}) diff --git a/components/frontend/src/widgets/TabPane.js b/components/frontend/src/widgets/TabPane.js index b89f7f1d9e..95e5a3ff20 100644 --- a/components/frontend/src/widgets/TabPane.js +++ b/components/frontend/src/widgets/TabPane.js @@ -7,9 +7,13 @@ import { Menu } from "semantic-ui-react" import { DarkMode } from "../context/DarkMode" import { Icon, Label, Tab } from "../semantic_ui_react_wrappers" -function FocusableTab({ error, iconName, image, label }) { +function FocusableTab({ error, iconName, image, label, warning }) { const className = useContext(DarkMode) ? "tabbutton inverted" : "tabbutton" - const tabLabel = error ? : label + let tabLabel = label + if (error || warning) { + const color = error ? "red" : "yellow" + tabLabel = + } return ( <> {iconName ? : image} @@ -22,6 +26,7 @@ FocusableTab.propTypes = { iconName: string, image: element, label: oneOfType([element, string]), + warning: bool, } export function tabPane(label, pane, options) { @@ -34,6 +39,7 @@ export function tabPane(label, pane, options) { iconName={options?.iconName} image={options?.image} label={label} + warning={options?.warning} /> ), diff --git a/components/frontend/src/widgets/TabPane.test.js b/components/frontend/src/widgets/TabPane.test.js index 4ff125aef0..0b1edf5456 100644 --- a/components/frontend/src/widgets/TabPane.test.js +++ b/components/frontend/src/widgets/TabPane.test.js @@ -23,6 +23,11 @@ it("shows the tab red when there is an error", () => { expect(screen.getByText("Tab").className).toEqual(expect.stringContaining("red")) }) +it("shows the tab yellow when there is a warning", () => { + render(Pane

, { warning: true })]} />) + expect(screen.getByText("Tab").className).toEqual(expect.stringContaining("yellow")) +}) + it("shows an icon", () => { const { container } = render(Pane

, { iconName: "server" })]} />) expect(container.firstChild.firstChild.firstChild.firstChild.className).toEqual(expect.stringContaining("server")) diff --git a/components/shared_code/.vulture_ignore_list.py b/components/shared_code/.vulture_ignore_list.py index e6b0c238d4..06587b20ae 100644 --- a/components/shared_code/.vulture_ignore_list.py +++ b/components/shared_code/.vulture_ignore_list.py @@ -1,8 +1,8 @@ _.subject_uuids # unused attribute (src/shared/model/report.py:36) _.check_description # unused method (src/shared_data_model/meta/base.py:37) -_.check_scales # unused method (src/shared_data_model/meta/data_model.py:22) -_.check_sources # unused method (src/shared_data_model/meta/data_model.py:38) -_.check_subjects # unused method (src/shared_data_model/meta/data_model.py:136) +_.check_scales # unused method (src/shared_data_model/meta/data_model.py:21) +_.check_sources # unused method (src/shared_data_model/meta/data_model.py:37) +_.check_subjects # unused method (src/shared_data_model/meta/data_model.py:135) ERROR # unused variable (src/shared_data_model/meta/entity.py:17) LEFT # unused variable (src/shared_data_model/meta/entity.py:37) model_config # unused variable (src/shared_data_model/meta/entity.py:45) @@ -32,7 +32,8 @@ parameter_layout # unused variable (src/shared_data_model/meta/source.py:26) issue_tracker # unused variable (src/shared_data_model/meta/source.py:28) supported_versions_description # unused variable (src/shared_data_model/meta/source.py:29) -_.check_parameters # unused method (src/shared_data_model/meta/source.py:31) +_.check_parameters # unused method (src/shared_data_model/meta/source.py:33) +_.check_deprecation_url # unused method (src/shared_data_model/meta/source.py:50) DOWNVOTES # unused variable (src/shared_data_model/meta/unit.py:16) min_value # unused variable (src/shared_data_model/parameters.py:28) _.check_unit # unused method (src/shared_data_model/parameters.py:30) diff --git a/components/shared_code/src/shared_data_model/meta/source.py b/components/shared_code/src/shared_data_model/meta/source.py index b29f78962f..9b4f3b34c0 100644 --- a/components/shared_code/src/shared_data_model/meta/source.py +++ b/components/shared_code/src/shared_data_model/meta/source.py @@ -27,10 +27,12 @@ class Source(DescribedModel): entities: dict[str, Entity] = {} issue_tracker: bool | None = False supported_versions_description: str | None = None # The source versions that Quality-time supports, e.g. "≥10.2" + deprecated: bool | None = False + deprecation_url: HttpUrl | None = None # URL to a GitHub issue @model_validator(mode="after") def check_parameters(self) -> Self: - """Check that if the source has a landing URL parameter it also has a URL parameter.""" + """Check the consistency of the source parameters.""" if "landing_url" in self.parameters and "url" not in self.parameters: msg = f"Source {self.name} has a landing URL but no URL" raise ValueError(msg) @@ -44,3 +46,11 @@ def check_parameters(self) -> Self: ) raise ValueError(msg) return self + + @model_validator(mode="after") + def check_deprecation_url(self) -> Self: + """Check that deprecated sources have a deprecation URL.""" + if self.deprecated and self.deprecation_url is None: + msg = f"Source {self.name} is deprecated but has no deprecation URL" + raise ValueError(msg) + return self diff --git a/components/shared_code/src/shared_data_model/sources/cxsast.py b/components/shared_code/src/shared_data_model/sources/cxsast.py index 23cff61f80..71e8b073ed 100644 --- a/components/shared_code/src/shared_data_model/sources/cxsast.py +++ b/components/shared_code/src/shared_data_model/sources/cxsast.py @@ -11,6 +11,8 @@ name="Checkmarx CxSAST", description="Static analysis software to identify security vulnerabilities in both custom code and open source " "components.", + deprecated=True, + deprecation_url=HttpUrl("https://github.com/ICTU/quality-time/issues/10383"), url=HttpUrl("https://checkmarx.com/glossary/static-application-security-testing-sast/"), parameters={ "project": StringParameter( diff --git a/components/shared_code/tests/shared_data_model/meta/test_source.py b/components/shared_code/tests/shared_data_model/meta/test_source.py index c6a7c813d2..0c7c80b6a6 100644 --- a/components/shared_code/tests/shared_data_model/meta/test_source.py +++ b/components/shared_code/tests/shared_data_model/meta/test_source.py @@ -28,10 +28,18 @@ def test_missing_parameter_to_validate_on(self): ) def test_missing_url_when_landing_url(self): - """Test that a source that has a landing url also has a url parameter.""" + """Test that a source that has a landing URL also has a URL parameter.""" extra_model_kwargs = { "parameters": { "landing_url": {"name": "URL", "type": "url", "metrics": ["metric"]}, }, } self.check_source_validation_error("Source Source has a landing URL but no URL", **extra_model_kwargs) + + def test_missing_url_when_deprecated(self): + """Test that a source that is deprecated also has a deprecation URL parameter.""" + extra_model_kwargs = {"deprecated": True, "parameters": {}} + self.check_source_validation_error( + "Source Source is deprecated but has no deprecation URL", + **extra_model_kwargs, + ) diff --git a/docs/src/changelog.md b/docs/src/changelog.md index 38207ec312..3f1c83138d 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -19,6 +19,10 @@ If your currently installed *Quality-time* version is not the latest version, pl - When measuring test cases with Visual Studio TRX as source, search all test category items for test case ids, instead of cutting the search short after the first match. Fixes [#10460](https://github.com/ICTU/quality-time/issues/10460). - Correctly parse empty Axe-core JSON report. Fixes [#10487](https://github.com/ICTU/quality-time/issues/10487). +### Changed + +- Support for Checkmarx CxSAST as source for metrics is deprecated. Closes [#10383](https://github.com/ICTU/quality-time/issues/10383). + ## v5.20.0 - 2024-12-05 ### Added diff --git a/docs/src/create_reference_md.py b/docs/src/create_reference_md.py index d4c9aa2c9d..9631c865be 100644 --- a/docs/src/create_reference_md.py +++ b/docs/src/create_reference_md.py @@ -170,6 +170,12 @@ def source_section(source: Source, source_key: str, level: int) -> str: if source.supported_versions_description: title = f"Supported {source.name} versions" markdown += admonition(source.supported_versions_description, title, "important") + if source.deprecated: + deprecation_message = ( + f"Support for using {source.name} as source is deprecated. " + f"See this [GitHub issue]({source.deprecation_url}) for more information." + ) + markdown += admonition(deprecation_message, "Deprecated", "caution") supported_metrics_markdown = "" metrics = [metric for metric in DATA_MODEL.metrics.values() if source_key in metric.sources] for metric in sorted(metrics, key=get_model_name):