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):