diff --git a/.changeset/cost-insights-fresh-radios-doubt.md b/.changeset/cost-insights-fresh-radios-doubt.md
new file mode 100644
index 0000000000..849f5140f6
--- /dev/null
+++ b/.changeset/cost-insights-fresh-radios-doubt.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-cost-insights': patch
+---
+
+Default alert properties can be overridden using accessors
diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index bf6dfb4451..2e0bd13962 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -94,6 +94,7 @@ Zalando
Zhou
Zolotusky
abc
+accessors
adamdmharvey
andrewthauer
api
diff --git a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx
new file mode 100644
index 0000000000..05a10bb2f0
--- /dev/null
+++ b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import pluralize from 'pluralize';
+import { renderInTestApp } from '@backstage/test-utils';
+import { ProjectGrowthAlert } from './ProjectGrowthAlert';
+import { ProjectGrowthData } from '../types';
+import {
+ MockCurrencyProvider,
+ MockConfigProvider,
+ MockBillingDateProvider,
+} from '../utils/tests';
+
+const mockData: ProjectGrowthData = {
+ project: 'test-project',
+ periodStart: '2021-01-01',
+ periodEnd: '2021-02-01',
+ aggregation: [0, 0],
+ change: {
+ ratio: 0,
+ amount: 0,
+ },
+ products: [
+ {
+ id: 'product-a',
+ aggregation: [0, 0],
+ },
+ ],
+};
+
+// suppress recharts componentDidUpdate deprecation warnings
+jest.spyOn(console, 'warn').mockImplementation(() => {});
+
+async function renderInContext(children: JSX.Element) {
+ return renderInTestApp(
+
+
+ {children}
+
+ ,
+ );
+}
+
+class CustomProjectGrowthAlert extends ProjectGrowthAlert {
+ get url() {
+ return 'path/to/resource';
+ }
+ get title() {
+ return `Investigate cost growth in ${pluralize(
+ 'project',
+ this.data.products.length,
+ true,
+ )}`;
+ }
+}
+
+describe('ProjectGrowthAlert', () => {
+ describe('constructor', () => {
+ it('should create a project growth alert', async () => {
+ const alert = new ProjectGrowthAlert(mockData);
+ const { getByText, queryByText } = await renderInContext(alert.element);
+
+ expect(alert.url).toBe('/cost-insights/investigating-growth');
+ expect(alert.title).toBe(
+ 'Investigate cost growth in project test-project',
+ );
+ expect(alert.subtitle).toBe(
+ 'Cost growth outpacing business growth is unsustainable long-term.',
+ );
+ expect(getByText('1 product')).toBeInTheDocument();
+ expect(queryByText('sorted by cost')).not.toBeInTheDocument();
+ });
+
+ it('a subclass can inherit and override defaults using accessors', async () => {
+ const alert = new CustomProjectGrowthAlert(mockData);
+ const { getByText, queryByText } = await renderInContext(alert.element);
+
+ expect(alert.url).toBe('path/to/resource');
+ expect(alert.title).toBe('Investigate cost growth in 1 project');
+ expect(alert.subtitle).toBe(
+ 'Cost growth outpacing business growth is unsustainable long-term.',
+ );
+ expect(getByText('1 product')).toBeInTheDocument();
+ expect(queryByText('sorted by cost')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx
index 67eb1b5071..088e85939d 100644
--- a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx
+++ b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx
@@ -27,18 +27,22 @@ import { Alert, ProjectGrowthData } from '../types';
export class ProjectGrowthAlert implements Alert {
data: ProjectGrowthData;
- url = '/cost-insights/investigating-growth';
- subtitle =
- 'Cost growth outpacing business growth is unsustainable long-term.';
-
constructor(data: ProjectGrowthData) {
this.data = data;
}
+ get url() {
+ return '/cost-insights/investigating-growth';
+ }
+
get title() {
return `Investigate cost growth in project ${this.data.project}`;
}
+ get subtitle() {
+ return 'Cost growth outpacing business growth is unsustainable long-term.';
+ }
+
get element() {
return ;
}
diff --git a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx
new file mode 100644
index 0000000000..95e213f06a
--- /dev/null
+++ b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import pluralize from 'pluralize';
+import { renderInTestApp } from '@backstage/test-utils';
+import { UnlabeledDataflowAlert } from './UnlabeledDataflowAlert';
+import { UnlabeledDataflowData } from '../types';
+import {
+ MockCurrencyProvider,
+ MockConfigProvider,
+ MockBillingDateProvider,
+} from '../utils/tests';
+
+const mockData: UnlabeledDataflowData = {
+ periodStart: '2021-02-01',
+ periodEnd: '2021-03-31',
+ unlabeledCost: 0,
+ labeledCost: 0,
+ projects: [
+ {
+ id: 'project-a',
+ labeledCost: 0,
+ unlabeledCost: 0,
+ },
+ ],
+};
+
+// suppress recharts componentDidUpdate deprecation warnings
+jest.spyOn(console, 'warn').mockImplementation(() => {});
+
+async function renderInContext(children: JSX.Element) {
+ return renderInTestApp(
+
+
+ {children}
+
+ ,
+ );
+}
+
+class CustomUnlabeledDataflowAlert extends UnlabeledDataflowAlert {
+ get url() {
+ return 'path/to/resource';
+ }
+ get title() {
+ return `Add labels to ${pluralize(
+ 'workflow',
+ this.data.projects.length,
+ true,
+ )}`;
+ }
+}
+
+describe('UnlabeledDataflowAlert', () => {
+ describe('constructor', () => {
+ it('should create an unlabeled dataflow alert', async () => {
+ const alert = new UnlabeledDataflowAlert(mockData);
+ const { getByText } = await renderInContext(alert.element);
+
+ expect(alert.url).toBe('/cost-insights/labeling-jobs');
+ expect(alert.title).toBe('Add labels to workflows');
+ expect(alert.subtitle).toBe(
+ 'Labels show in billing data, enabling cost insights for each workflow.',
+ );
+ expect(
+ getByText(
+ 'Showing costs from 1 project with unlabeled Dataflow jobs in the last 30 days.',
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it('a subclass can inherit and override defaults using accessors', async () => {
+ const alert = new CustomUnlabeledDataflowAlert(mockData);
+ const { getByText } = await renderInContext(alert.element);
+
+ expect(alert.url).toBe('path/to/resource');
+ expect(alert.title).toBe('Add labels to 1 workflow');
+ expect(alert.subtitle).toBe(
+ 'Labels show in billing data, enabling cost insights for each workflow.',
+ );
+ expect(
+ getByText(
+ 'Showing costs from 1 project with unlabeled Dataflow jobs in the last 30 days.',
+ ),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx
index e889e0e3d4..7d11f3cc81 100644
--- a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx
+++ b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx
@@ -28,15 +28,22 @@ export class UnlabeledDataflowAlert implements Alert {
data: UnlabeledDataflowData;
status?: AlertStatus;
- url = '/cost-insights/labeling-jobs';
- title = 'Add labels to workflows';
- subtitle =
- 'Labels show in billing data, enabling cost insights for each workflow.';
-
constructor(data: UnlabeledDataflowData) {
this.data = data;
}
+ get url() {
+ return '/cost-insights/labeling-jobs';
+ }
+
+ get title() {
+ return 'Add labels to workflows';
+ }
+
+ get subtitle() {
+ return 'Labels show in billing data, enabling cost insights for each workflow.';
+ }
+
get element() {
return ;
}