From 668c7f79cf8c4cb2df590f2babb5ded504a12cc6 Mon Sep 17 00:00:00 2001 From: Milenko Tomic Date: Tue, 23 Apr 2024 14:22:31 -0400 Subject: [PATCH 1/7] refactor: move common components to its own folder --- .../InsightsCard.tsx | 0 .../OpenServiceButton.tsx | 0 .../ServiceStandardsCard.tsx | 0 .../StatusCard.tsx | 0 .../TriggerIncidentButton.tsx | 0 src/components/PagerDutyCardCommon/index.ts | 13 +++++++++++++ 6 files changed, 13 insertions(+) rename src/components/{PagerDutyCard => PagerDutyCardCommon}/InsightsCard.tsx (100%) rename src/components/{PagerDutyCard => PagerDutyCardCommon}/OpenServiceButton.tsx (100%) rename src/components/{PagerDutyCard => PagerDutyCardCommon}/ServiceStandardsCard.tsx (100%) rename src/components/{PagerDutyCard => PagerDutyCardCommon}/StatusCard.tsx (100%) rename src/components/{PagerDutyCard => PagerDutyCardCommon}/TriggerIncidentButton.tsx (100%) create mode 100644 src/components/PagerDutyCardCommon/index.ts diff --git a/src/components/PagerDutyCard/InsightsCard.tsx b/src/components/PagerDutyCardCommon/InsightsCard.tsx similarity index 100% rename from src/components/PagerDutyCard/InsightsCard.tsx rename to src/components/PagerDutyCardCommon/InsightsCard.tsx diff --git a/src/components/PagerDutyCard/OpenServiceButton.tsx b/src/components/PagerDutyCardCommon/OpenServiceButton.tsx similarity index 100% rename from src/components/PagerDutyCard/OpenServiceButton.tsx rename to src/components/PagerDutyCardCommon/OpenServiceButton.tsx diff --git a/src/components/PagerDutyCard/ServiceStandardsCard.tsx b/src/components/PagerDutyCardCommon/ServiceStandardsCard.tsx similarity index 100% rename from src/components/PagerDutyCard/ServiceStandardsCard.tsx rename to src/components/PagerDutyCardCommon/ServiceStandardsCard.tsx diff --git a/src/components/PagerDutyCard/StatusCard.tsx b/src/components/PagerDutyCardCommon/StatusCard.tsx similarity index 100% rename from src/components/PagerDutyCard/StatusCard.tsx rename to src/components/PagerDutyCardCommon/StatusCard.tsx diff --git a/src/components/PagerDutyCard/TriggerIncidentButton.tsx b/src/components/PagerDutyCardCommon/TriggerIncidentButton.tsx similarity index 100% rename from src/components/PagerDutyCard/TriggerIncidentButton.tsx rename to src/components/PagerDutyCardCommon/TriggerIncidentButton.tsx diff --git a/src/components/PagerDutyCardCommon/index.ts b/src/components/PagerDutyCardCommon/index.ts new file mode 100644 index 0000000..e177a55 --- /dev/null +++ b/src/components/PagerDutyCardCommon/index.ts @@ -0,0 +1,13 @@ +import InsightsCard from './InsightsCard'; +import {OpenServiceButton} from './OpenServiceButton'; +import ServiceStandardsCard from './ServiceStandardsCard'; +import StatusCard from './StatusCard'; +import {TriggerIncidentButton} from './TriggerIncidentButton'; + +export { + InsightsCard, + OpenServiceButton, + ServiceStandardsCard, + StatusCard, + TriggerIncidentButton +} From ef1404d1f2e8f163db36b14d15e8fef6e5fefe94 Mon Sep 17 00:00:00 2001 From: Milenko Tomic Date: Tue, 23 Apr 2024 14:22:54 -0400 Subject: [PATCH 2/7] refactor: update PagerDutyCard imports --- src/components/PagerDutyCard/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/PagerDutyCard/index.tsx b/src/components/PagerDutyCard/index.tsx index 08d75ee..cad8bfd 100644 --- a/src/components/PagerDutyCard/index.tsx +++ b/src/components/PagerDutyCard/index.tsx @@ -42,14 +42,16 @@ import { } from "@backstage/core-components"; import { PagerDutyEntity } from "../../types"; import { ForbiddenError } from "../Errors/ForbiddenError"; -import { TriggerIncidentButton } from "./TriggerIncidentButton"; -import { OpenServiceButton } from "./OpenServiceButton"; +import { + InsightsCard, + OpenServiceButton, + ServiceStandardsCard, + StatusCard, + TriggerIncidentButton +} from "../PagerDutyCardCommon"; import { createStyles, makeStyles, useTheme } from "@material-ui/core/styles"; -import StatusCard from "./StatusCard"; -import ServiceStandardsCard from "./ServiceStandardsCard"; import { BackstageTheme } from "@backstage/theme"; import { PagerDutyCardServiceResponse } from "../../api/types"; -import InsightsCard from "./InsightsCard"; const useStyles = makeStyles((theme) => createStyles({ From 29960e05f3723409f949a2a5b05528ea3d363c9d Mon Sep 17 00:00:00 2001 From: Milenko Tomic Date: Tue, 23 Apr 2024 14:23:41 -0400 Subject: [PATCH 3/7] feat: create PagerDutySmallCard component --- .../EntityPagerDutySmallCard/index.test.tsx | 468 ++++++++++++++++++ .../EntityPagerDutySmallCard/index.tsx | 51 ++ .../PagerDutySmallCard/index.test.tsx | 334 +++++++++++++ src/components/PagerDutySmallCard/index.tsx | 299 +++++++++++ src/components/index.ts | 6 + src/index.ts | 1 + src/plugin.ts | 13 + 7 files changed, 1172 insertions(+) create mode 100644 src/components/EntityPagerDutySmallCard/index.test.tsx create mode 100644 src/components/EntityPagerDutySmallCard/index.tsx create mode 100644 src/components/PagerDutySmallCard/index.test.tsx create mode 100644 src/components/PagerDutySmallCard/index.tsx diff --git a/src/components/EntityPagerDutySmallCard/index.test.tsx b/src/components/EntityPagerDutySmallCard/index.test.tsx new file mode 100644 index 0000000..7b18cff --- /dev/null +++ b/src/components/EntityPagerDutySmallCard/index.test.tsx @@ -0,0 +1,468 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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. + */ +// eslint-disable-next-line @backstage/no-undeclared-imports +import React from "react"; +import { render, waitFor, fireEvent, act } from "@testing-library/react"; +import { + EntityPagerDutyCard, + isPluginApplicableToEntity, +} from "../EntityPagerDutyCard"; +import { Entity } from "@backstage/catalog-model"; +import { EntityProvider } from "@backstage/plugin-catalog-react"; +import { NotFoundError } from "@backstage/errors"; +import { TestApiRegistry, wrapInTestApp } from "@backstage/test-utils"; +import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from "../../api"; +import { + PagerDutyUser, + PagerDutyService, + PagerDutyServiceStandards, + PagerDutyServiceMetrics, +} from "@pagerduty/backstage-plugin-common"; + +import { alertApiRef } from "@backstage/core-plugin-api"; +import { ApiProvider } from "@backstage/core-app-api"; + +const entity: Entity = { + apiVersion: "backstage.io/v1alpha1", + kind: "Component", + metadata: { + name: "pagerduty-test", + annotations: { + "pagerduty.com/integration-key": "abc123", + }, + }, +}; + +const entityWithoutAnnotations: Entity = { + apiVersion: "backstage.io/v1alpha1", + kind: "Component", + metadata: { + name: "pagerduty-test", + annotations: {}, + }, +}; + +const entityWithServiceId: Entity = { + apiVersion: "backstage.io/v1alpha1", + kind: "Component", + metadata: { + name: "pagerduty-test", + annotations: { + "pagerduty.com/service-id": "def456", + }, + }, +}; + +const entityWithAllAnnotations: Entity = { + apiVersion: "backstage.io/v1alpha1", + kind: "Component", + metadata: { + name: "pagerduty-test", + annotations: { + "pagerduty.com/integration-key": "abc123", + "pagerduty.com/service-id": "def456", + }, + }, +}; + +const service: PagerDutyService = { + id: "SERV1CE1D", + name: "service-one", + html_url: "www.example.com", + escalation_policy: { + id: "ESCALAT1ONP01ICY1D", + name: "ep-one", + html_url: "http://www.example.com/escalation-policy/ESCALAT1ONP01ICY1D", + }, +}; + +const oncallUsers: PagerDutyUser[] = []; + +const standards: PagerDutyServiceStandards = { + resource_id: "RES0URCE1D", + resource_type: "technical_service", + score: { + passing: 1, + total: 1, + }, + standards: [ + { + active: true, + description: "Standard description", + id: "STANDARD1D", + name: "Standard name", + pass: true, + type: "technical_service_standard", + }, + ], +}; + +const metrics: PagerDutyServiceMetrics[] = [ + { + service_id: "SERV1CE1D", + total_high_urgency_incidents: 5, + total_incident_count: 12, + total_interruptions: 3, + }, +]; + +const mockPagerDutyApi: Partial = { + getServiceByEntity: async () => ({ service }), + getServiceByPagerDutyEntity: async () => ({ service }), + getOnCallByPolicyId: async () => oncallUsers, + getIncidentsByServiceId: async () => ({ incidents: [] }), + getServiceStandardsByServiceId: async () => ({ standards }), + getServiceMetricsByServiceId: async () => ({ metrics }), +}; + +const apis = TestApiRegistry.from( + [pagerDutyApiRef, mockPagerDutyApi], + [alertApiRef, {}] +); + +describe("isPluginApplicableToEntity", () => { + describe("when entity has no annotations", () => { + it("returns false", () => { + expect(isPluginApplicableToEntity(entityWithoutAnnotations)).toBe(false); + }); + }); + + describe("when entity has the pagerduty.com/integration-key annotation", () => { + it("returns true", () => { + expect(isPluginApplicableToEntity(entity)).toBe(true); + }); + }); + + describe("when entity has the pagerduty.com/service-id annotation", () => { + it("returns true", () => { + expect(isPluginApplicableToEntity(entityWithServiceId)).toBe(true); + }); + }); + + describe("when entity has all annotations", () => { + it("returns true", () => { + expect(isPluginApplicableToEntity(entityWithAllAnnotations)).toBe(true); + }); + }); +}); + +describe("EntityPagerDutyCard", () => { + it("Render pagerduty", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(getByText("Create new incident")).toBeInTheDocument(); + expect(getByText("Nice! No incidents found!")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + }); + + it("Handles custom error for missing token", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Missing or invalid PagerDuty Token")).toBeInTheDocument(); + }); + + it("Handles custom NotFoundError", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new NotFoundError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("PagerDuty Service Not Found")).toBeInTheDocument(); + }); + + it("handles general error", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new Error("An error occurred")); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + + expect( + getByText( + "You don't have the required permissions to perform this action. See README for more details." + ) + ).toBeInTheDocument(); + }); + + it("opens the dialog when trigger button is clicked", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId, getByRole } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + + const triggerLink = getByText("Create new incident"); + await act(async () => { + fireEvent.click(triggerLink); + }); + expect(getByRole("dialog")).toBeInTheDocument(); + }); + + describe("when entity has the pagerduty.com/service-id annotation", () => { + it("Renders PagerDuty service information", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(queryByTestId("trigger-incident-button")).not.toBeInTheDocument(); + expect(getByText("Nice! No incidents found!")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + }); + + it("Handles custom error for missing token", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId("progress")); + expect( + getByText("Missing or invalid PagerDuty Token") + ).toBeInTheDocument(); + }); + + it("Handles custom NotFoundError", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new NotFoundError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("PagerDuty Service Not Found")).toBeInTheDocument(); + }); + + it("handles general error", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new Error("An error occurred")); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + + expect( + getByText( + "You don't have the required permissions to perform this action. See README for more details." + ) + ).toBeInTheDocument(); + }); + + it("hides the Create new incident button", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(queryByTestId("trigger-incident-button")).not.toBeInTheDocument(); + }); + }); + + describe("when entity has all annotations", () => { + it("queries by integration key", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(getByText("Create new incident")).toBeInTheDocument(); + expect(getByText("Nice! No incidents found!")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + }); + }); + + describe("when entity has all annotations but the plugin has been configured to disable change events", () => { + it("must hide change events tab", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(queryByTestId("change-events")).not.toBeInTheDocument(); + expect(getByText("Nice! No incidents found!")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + }); + }); + + describe("when entity has all annotations but the plugin has been configured to disable on-call", () => { + it("must hide on-call component", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(getByText("Change Events")).toBeInTheDocument(); + expect(getByText("Nice! No incidents found!")).toBeInTheDocument(); + expect(queryByTestId("oncall-card")).not.toBeInTheDocument(); + }); + }); + + describe('when entity has all annotations but the plugin has been configured to be "read only"', () => { + it('queries by integration key but does not render the "Create new incident" button', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(getByText("Nice! No incidents found!")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + expect(() => getByText("Create new incident")).toThrow(); + }); + }); +}); diff --git a/src/components/EntityPagerDutySmallCard/index.tsx b/src/components/EntityPagerDutySmallCard/index.tsx new file mode 100644 index 0000000..7d9a135 --- /dev/null +++ b/src/components/EntityPagerDutySmallCard/index.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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. + */ +// eslint-disable-next-line @backstage/no-undeclared-imports +import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { getPagerDutyEntity } from '../pagerDutyEntity'; +import { PagerDutySmallCard } from '../PagerDutySmallCard'; + +/** @public */ +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean( + entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY] || + entity.metadata.annotations?.[PAGERDUTY_SERVICE_ID], + ); + +/** @public */ +export type EntityPagerDutySmallCardProps = { + readOnly?: boolean; + disableInsights?: boolean; + disableOnCall?: boolean; +}; + +/** @public */ +export const EntityPagerDutySmallCard = (props: EntityPagerDutySmallCardProps) => { + const { readOnly, disableInsights, disableOnCall } = props; + const { entity } = useEntity(); + const pagerDutyEntity = getPagerDutyEntity(entity); + return ( + + ); +}; diff --git a/src/components/PagerDutySmallCard/index.test.tsx b/src/components/PagerDutySmallCard/index.test.tsx new file mode 100644 index 0000000..06ca3c3 --- /dev/null +++ b/src/components/PagerDutySmallCard/index.test.tsx @@ -0,0 +1,334 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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. + */ +// eslint-disable-next-line @backstage/no-undeclared-imports +import React from "react"; +import { render, waitFor, fireEvent, act } from "@testing-library/react"; +import { PagerDutySmallCard } from "../PagerDutySmallCard"; +import { NotFoundError } from "@backstage/errors"; +import { TestApiRegistry, wrapInTestApp } from "@backstage/test-utils"; +import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from "../../api"; +import { + PagerDutyService, + PagerDutyServiceMetrics, + PagerDutyServiceStandards, +} from "@pagerduty/backstage-plugin-common"; + +import { alertApiRef } from "@backstage/core-plugin-api"; +import { ApiProvider } from "@backstage/core-app-api"; + +const service: PagerDutyService = { + id: "SERV1CE1D", + name: "service-one", + html_url: "www.example.com", + escalation_policy: { + id: "ESCALAT1ONP01ICY1D", + name: "ep-one", + html_url: "http://www.example.com/escalation-policy/ESCALAT1ONP01ICY1D", + }, +}; + +const standards: PagerDutyServiceStandards = { + resource_id: "SERV1CE1D", + resource_type: "technical_service", + score: { + passing: 1, + total: 1, + }, + standards: [ + { + id: "STANDARD1D", + name: "standard-one", + description: "standard-one-description", + active: true, + pass: true, + type: "technical_service", + }, + ], +}; + +const metrics: PagerDutyServiceMetrics[] = [ + { + service_id: "SERV1CE1D", + total_incident_count: 10, + total_high_urgency_incidents: 5, + total_interruptions: 5, + }, +]; + +const mockPagerDutyApi: Partial = { + getServiceByEntity: async () => ({ service }), + getOnCallByPolicyId: async () => [], + getServiceStandardsByServiceId: async () => ({ standards }), + getServiceMetricsByServiceId: async () => ({ metrics }), +}; + +const apis = TestApiRegistry.from( + [pagerDutyApiRef, mockPagerDutyApi], + [alertApiRef, {}] +); + +describe("PagerDutySmallCard", () => { + it("Render pagerduty", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(getByText("Create new incident")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + }); + + it("Handles custom error for missing token", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Missing or invalid PagerDuty Token")).toBeInTheDocument(); + }); + + it("Handles custom NotFoundError", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new NotFoundError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("PagerDuty Service Not Found")).toBeInTheDocument(); + }); + + it("handles general error", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new Error("An error occurred")); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + + expect( + getByText( + "You don't have the required permissions to perform this action. See README for more details." + ) + ).toBeInTheDocument(); + }); + + it("opens the dialog when trigger button is clicked", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId, getByRole } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + + const triggerLink = getByText("Create new incident"); + await act(async () => { + fireEvent.click(triggerLink); + }); + expect(getByRole("dialog")).toBeInTheDocument(); + }); + + describe("when entity has the pagerduty.com/service-id annotation", () => { + it("Renders PagerDuty service information", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(getByText("Create new incident")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + }); + + it("Handles custom error for missing token", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect( + getByText("Missing or invalid PagerDuty Token") + ).toBeInTheDocument(); + }); + + it("Handles custom NotFoundError", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new NotFoundError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("PagerDuty Service Not Found")).toBeInTheDocument(); + }); + + it("handles general error", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockRejectedValueOnce(new Error("An error occurred")); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + + expect( + getByText( + "You don't have the required permissions to perform this action. See README for more details." + ) + ).toBeInTheDocument(); + }); + + it("hides the Create new incident button", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(queryByTestId("trigger-incident-button")).not.toBeInTheDocument(); + }); + }); + + describe("when entity has all annotations", () => { + it("queries by integration key", async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect(getByText("Create new incident")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + }); + }); + + describe('when entity has all annotations but the plugin has been configured to be "read only"', () => { + it('queries by integration key but does not render the "Create new incident" button', async () => { + mockPagerDutyApi.getServiceByPagerDutyEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + ) + ); + await waitFor(() => !queryByTestId("progress")); + expect(getByText("Open service in PagerDuty")).toBeInTheDocument(); + expect( + getByText("No one is on-call. Update the escalation policy.") + ).toBeInTheDocument(); + expect(() => getByText("Create new incident")).toThrow(); + }); + }); +}); diff --git a/src/components/PagerDutySmallCard/index.tsx b/src/components/PagerDutySmallCard/index.tsx new file mode 100644 index 0000000..eb62041 --- /dev/null +++ b/src/components/PagerDutySmallCard/index.tsx @@ -0,0 +1,299 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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. + */ +// eslint-disable-next-line @backstage/no-undeclared-imports +import React, {ReactNode, useCallback, useState} from "react"; +import { + Card, + CardHeader, + Divider, + CardContent, + Grid, + Typography, +} from "@material-ui/core"; +import {EscalationPolicy} from "../Escalation"; +import useAsync from "react-use/lib/useAsync"; +import {pagerDutyApiRef, UnauthorizedError} from "../../api"; +import {MissingTokenError, ServiceNotFoundError} from "../Errors"; +import PDGreenImage from "../../assets/PD-Green.svg"; +import PDWhiteImage from "../../assets/PD-White.svg"; + +import {useApi} from "@backstage/core-plugin-api"; +import {NotFoundError} from "@backstage/errors"; +import { + Progress, + InfoCard, +} from "@backstage/core-components"; +import {PagerDutyEntity} from "../../types"; +import {ForbiddenError} from "../Errors/ForbiddenError"; +import { + InsightsCard, + OpenServiceButton, + ServiceStandardsCard, + StatusCard, + TriggerIncidentButton +} from "../PagerDutyCardCommon"; +import {createStyles, makeStyles, useTheme} from "@material-ui/core/styles"; +import {BackstageTheme} from "@backstage/theme"; +import {PagerDutyCardServiceResponse} from "../../api/types"; + +const useStyles = makeStyles((theme) => + createStyles({ + overviewHeaderTextStyle: { + fontSize: "14px", + fontWeight: 500, + color: + theme.palette.type === "light" + ? "rgba(0, 0, 0, 0.54)" + : "rgba(255, 255, 255, 0.7)", + }, + headerStyle: { + marginBottom: "0px", + fontSize: "0px", + }, + overviewHeaderContainerStyle: { + display: "flex", + margin: "0px", + padding: "15px", + marginBottom: "20px", + }, + headerWithSubheaderContainerStyle: { + display: "flex", + alignItems: "center", + }, + subheaderTextStyle: { + fontSize: "10px", + marginLeft: "5px", + }, + overviewCardsContainerStyle: { + display: "flex", + margin: "15px", + marginTop: "-15px", + }, + incidentMetricsContainerStyle: { + display: "flex", + height: "100%", + justifyContent: "center", + columnSpan: "all", + margin: "15px", + marginTop: "-15px", + }, + }) +); + +const BasicCard = ({ children }: { children: ReactNode }) => ( + {children} +); + +/** @public */ +export type PagerDutyCardProps = PagerDutyEntity & { + readOnly?: boolean; + disableInsights?: boolean; + disableOnCall?: boolean; +}; + +/** @public */ +export const PagerDutySmallCard = (props: PagerDutyCardProps) => { + const classes = useStyles(); + + const theme = useTheme(); + const { readOnly, disableInsights, disableOnCall } = props; + const api = useApi(pagerDutyApiRef); + const [refreshStatus, setRefreshStatus] = useState(false); + + const handleRefresh = useCallback(() => { + setRefreshStatus((x) => !x); + }, []); + + const { + value: service, + loading, + error, + } = useAsync(async () => { + const { service: foundService } = await api.getServiceByPagerDutyEntity( + props + ); + + const serviceStandards = await api.getServiceStandardsByServiceId( + foundService.id + ); + + const serviceMetrics = await api.getServiceMetricsByServiceId( + foundService.id + ); + + const result: PagerDutyCardServiceResponse = { + id: foundService.id, + name: foundService.name, + url: foundService.html_url, + policyId: foundService.escalation_policy.id, + policyLink: foundService.escalation_policy.html_url as string, + policyName: foundService.escalation_policy.name, + status: foundService.status, + standards: + serviceStandards !== undefined ? serviceStandards.standards : undefined, + metrics: + serviceMetrics !== undefined ? serviceMetrics.metrics : undefined, + }; + + return result; + }, [props]); + + if (error) { + let errorNode: ReactNode; + + switch (error.constructor) { + case UnauthorizedError: + errorNode = ; + break; + case NotFoundError: + errorNode = ; + break; + default: + errorNode = ; + } + + return {errorNode}; + } + + if (loading) { + return ( + + + + ); + } + + return ( + + + ) : ( + PagerDuty + ) + } + action={ + (!readOnly && props.integrationKey) ? ( +
+ + +
+ ) : ( + + ) + } + /> + + + + STATUS + + + + + STANDARDS + + + + + + + + + + + + + {disableInsights !== true ? ( + <> + + + + INSIGHTS + + + (last 30 days) + + + + + 0 + ? service?.metrics[0].total_interruptions + : undefined} + label="interruptions" + color={theme.palette.textSubtle}/> + + + 0 + ? service?.metrics[0].total_high_urgency_incidents + : undefined} + label="high urgency" + color={theme.palette.warning.main}/> + + + 0 + ? service?.metrics[0].total_incident_count + : undefined} + label="incidents" + color={theme.palette.error.main}/> + + + + ) : ( + <> + )} + + + {disableOnCall !== true ? ( + + ) : ( + <> + )} + +
+ ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 4e3f753..25a97e9 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -16,6 +16,7 @@ export type { EntityPagerDutyCardProps } from './EntityPagerDutyCard'; +export type { EntityPagerDutySmallCardProps } from './EntityPagerDutySmallCard'; export type { HomePagePagerDutyCardProps } from './HomePagePagerDutyCard'; export { @@ -24,4 +25,9 @@ export { EntityPagerDutyCard, } from './EntityPagerDutyCard'; +export { + isPluginApplicableToEntity as isPagerDutySmallCardAvailable, + EntityPagerDutySmallCard, +} from './EntityPagerDutySmallCard'; + export { TriggerButton } from './TriggerButton'; diff --git a/src/index.ts b/src/index.ts index c6f7aef..945ab8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export { pagerDutyPlugin as plugin, EntityPagerDutyCard, HomePagePagerDutyCard, + EntityPagerDutySmallCard, } from './plugin'; export * from './components'; diff --git a/src/plugin.ts b/src/plugin.ts index 59cc5cd..e58595e 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -60,6 +60,19 @@ export const EntityPagerDutyCard = pagerDutyPlugin.provide( }), ); +/** @public */ +export const EntityPagerDutySmallCard = pagerDutyPlugin.provide( + createComponentExtension({ + name: 'EntityPagerDutySmallCard', + component: { + lazy: () => + import('./components/EntityPagerDutySmallCard').then( + m => m.EntityPagerDutySmallCard, + ), + }, + }), +); + /** @public */ export const HomePagePagerDutyCard = pagerDutyPlugin.provide( createCardExtension({ From 2342480f3f38b4722b13ddfa0eff200b74bb14a5 Mon Sep 17 00:00:00 2001 From: Milenko Tomic Date: Tue, 23 Apr 2024 14:24:27 -0400 Subject: [PATCH 4/7] feat: add PagerDutySmallCard to dev --- dev/index.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/dev/index.tsx b/dev/index.tsx index fb3aee0..353f462 100644 --- a/dev/index.tsx +++ b/dev/index.tsx @@ -18,10 +18,11 @@ import React from 'react'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { createDevApp } from '@backstage/dev-utils'; -import { pagerDutyPlugin, EntityPagerDutyCard } from '../src/plugin'; +import {pagerDutyPlugin, EntityPagerDutyCard, EntityPagerDutySmallCard} from '../src/plugin'; import { pagerDutyApiRef } from '../src/api'; import { mockPagerDutyApi } from './mockPagerDutyApi'; import { mockEntity } from './mockEntity'; +import {Grid} from '@material-ui/core'; createDevApp() .registerApi({ @@ -39,4 +40,15 @@ createDevApp() ), }) + .addPage({ + path: '/pagerduty-small', + title: 'PagerDuty Small Card', + element: ( + + + + + + ), + }) .render(); From 21debaa83f6a1900e44d4993614ac5993474bcb1 Mon Sep 17 00:00:00 2001 From: Tiago Barbosa Date: Mon, 29 Apr 2024 23:06:26 +0100 Subject: [PATCH 5/7] refactor: :children_crossing: adding accordions for collapsable sections of the small PagerDuty card Signed-off-by: Tiago Barbosa --- .../Escalation/EscalationPolicy.tsx | 1 - src/components/Escalation/EscalationUser.tsx | 48 +++-- src/components/PagerDutyCard/index.tsx | 28 ++- src/components/PagerDutySmallCard/index.tsx | 179 +++++++++++------- 4 files changed, 164 insertions(+), 92 deletions(-) diff --git a/src/components/Escalation/EscalationPolicy.tsx b/src/components/Escalation/EscalationPolicy.tsx index 55e7bfd..0518eae 100644 --- a/src/components/Escalation/EscalationPolicy.tsx +++ b/src/components/Escalation/EscalationPolicy.tsx @@ -74,7 +74,6 @@ export const EscalationPolicy = ({ return ( ON CALL} style={{ marginLeft: "-15px" }} > {users!.map((user, index) => ( diff --git a/src/components/Escalation/EscalationUser.tsx b/src/components/Escalation/EscalationUser.tsx index 2ad6001..06f2d1a 100644 --- a/src/components/Escalation/EscalationUser.tsx +++ b/src/components/Escalation/EscalationUser.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { ListItem, ListItemIcon, - ListItemSecondaryAction, + // ListItemSecondaryAction, Tooltip, ListItemText, makeStyles, @@ -29,7 +29,7 @@ import Avatar from '@material-ui/core/Avatar'; import { PagerDutyUser } from '@pagerduty/backstage-plugin-common'; import NotificationsIcon from "@material-ui/icons/Notifications"; import { BackstageTheme } from '@backstage/theme'; -import OpenInBrowser from '@material-ui/icons/OpenInBrowser'; +// import OpenInBrowser from '@material-ui/icons/OpenInBrowser'; const useStyles = makeStyles((theme) => ({ listItemPrimary: { @@ -50,6 +50,17 @@ const useStyles = makeStyles((theme) => ({ textDecoration: "underline", }, }, + userTextButtonStyle: { + marginLeft: "-11px", + marginTop: "-10px", + marginBottom: "-10px", + fontSize: "15px", + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "transparent", + textDecoration: "underline", + }, + }, containerStyle: { display: "flex", alignItems: "center", @@ -64,7 +75,7 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.primary, }, avatarStyle: { - marginTop: "-20px" + marginTop: "-20px", }, })); @@ -74,7 +85,7 @@ type Props = { policyName: string; }; -function navigateToEscalationPolicy(url: string) { +function navigateToUrl(url: string) { // open url in new browser window window.open(url, "_blank"); } @@ -85,14 +96,25 @@ export const EscalationUser = ({ user, policyUrl, policyName }: Props) => { return ( - + - - {user.name} - + + navigateToUrl(user.html_url)} + className={classes.userTextButtonStyle} + > + {user.name} + + + { } secondary={ navigateToEscalationPolicy(policyUrl)} + aria-label="open-escalation-policy-in-browser" + onClick={() => navigateToUrl(policyUrl)} className={classes.buttonStyle} > @@ -114,18 +136,18 @@ export const EscalationUser = ({ user, policyUrl, policyName }: Props) => { } /> - + {/* - + */} ); }; diff --git a/src/components/PagerDutyCard/index.tsx b/src/components/PagerDutyCard/index.tsx index cad8bfd..b5175c7 100644 --- a/src/components/PagerDutyCard/index.tsx +++ b/src/components/PagerDutyCard/index.tsx @@ -63,6 +63,15 @@ const useStyles = makeStyles((theme) => ? "rgba(0, 0, 0, 0.54)" : "rgba(255, 255, 255, 0.7)", }, + oncallHeaderTextStyle: { + fontSize: "14px", + fontWeight: 500, + marginTop: "10px", + color: + theme.palette.type === "light" + ? "rgba(0, 0, 0, 0.54)" + : "rgba(255, 255, 255, 0.7)", + }, headerStyle: { marginBottom: "0px", fontSize: "0px", @@ -194,7 +203,7 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { ) } action={ - (!readOnly && props.integrationKey) ? ( + !readOnly && props.integrationKey ? (
{ )} {disableOnCall !== true ? ( - + <> + + ON CALL + + + ) : ( <> )} diff --git a/src/components/PagerDutySmallCard/index.tsx b/src/components/PagerDutySmallCard/index.tsx index eb62041..3d720e8 100644 --- a/src/components/PagerDutySmallCard/index.tsx +++ b/src/components/PagerDutySmallCard/index.tsx @@ -14,40 +14,39 @@ * limitations under the License. */ // eslint-disable-next-line @backstage/no-undeclared-imports -import React, {ReactNode, useCallback, useState} from "react"; +import React, { ReactNode, useCallback, useState } from "react"; import { + Accordion, + AccordionDetails, + AccordionSummary, Card, CardHeader, - Divider, - CardContent, Grid, Typography, } from "@material-ui/core"; -import {EscalationPolicy} from "../Escalation"; import useAsync from "react-use/lib/useAsync"; -import {pagerDutyApiRef, UnauthorizedError} from "../../api"; -import {MissingTokenError, ServiceNotFoundError} from "../Errors"; +import { pagerDutyApiRef, UnauthorizedError } from "../../api"; +import { MissingTokenError, ServiceNotFoundError } from "../Errors"; import PDGreenImage from "../../assets/PD-Green.svg"; import PDWhiteImage from "../../assets/PD-White.svg"; -import {useApi} from "@backstage/core-plugin-api"; -import {NotFoundError} from "@backstage/errors"; -import { - Progress, - InfoCard, -} from "@backstage/core-components"; -import {PagerDutyEntity} from "../../types"; -import {ForbiddenError} from "../Errors/ForbiddenError"; +import { useApi } from "@backstage/core-plugin-api"; +import { NotFoundError } from "@backstage/errors"; +import { Progress, InfoCard } from "@backstage/core-components"; +import { PagerDutyEntity } from "../../types"; +import { ForbiddenError } from "../Errors/ForbiddenError"; import { InsightsCard, OpenServiceButton, ServiceStandardsCard, StatusCard, - TriggerIncidentButton + TriggerIncidentButton, } from "../PagerDutyCardCommon"; -import {createStyles, makeStyles, useTheme} from "@material-ui/core/styles"; -import {BackstageTheme} from "@backstage/theme"; -import {PagerDutyCardServiceResponse} from "../../api/types"; +import { createStyles, makeStyles, useTheme } from "@material-ui/core/styles"; +import { BackstageTheme } from "@backstage/theme"; +import { PagerDutyCardServiceResponse } from "../../api/types"; +import { EscalationPolicy } from "../Escalation"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; const useStyles = makeStyles((theme) => createStyles({ @@ -67,7 +66,7 @@ const useStyles = makeStyles((theme) => display: "flex", margin: "0px", padding: "15px", - marginBottom: "20px", + marginBottom: "5px", }, headerWithSubheaderContainerStyle: { display: "flex", @@ -82,6 +81,12 @@ const useStyles = makeStyles((theme) => margin: "15px", marginTop: "-15px", }, + onCallAccordionDetails: { + display: "flex", + width: "100%", + marginTop: "-25px", + marginBottom: "-15px", + }, incidentMetricsContainerStyle: { display: "flex", height: "100%", @@ -156,13 +161,13 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { switch (error.constructor) { case UnauthorizedError: - errorNode = ; + errorNode = ; break; case NotFoundError: - errorNode = ; + errorNode = ; break; default: - errorNode = ; + errorNode = ; } return {errorNode}; @@ -171,7 +176,7 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { if (loading) { return ( - + ); } @@ -182,13 +187,13 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { className={classes.headerStyle} title={ theme.palette.type === "dark" ? ( - PagerDuty + PagerDuty ) : ( - PagerDuty + PagerDuty ) } action={ - (!readOnly && props.integrationKey) ? ( + !readOnly && props.integrationKey ? (
{ entityName={props.name} handleRefresh={handleRefresh} /> - +
) : ( - + ) } /> @@ -218,7 +223,7 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { - + { {disableInsights !== true ? ( - <> - + + } + aria-controls="panel1a-content" + id="panel1a-header" + > INSIGHTS @@ -251,49 +260,77 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { (last 30 days) - - - 0 - ? service?.metrics[0].total_interruptions - : undefined} - label="interruptions" - color={theme.palette.textSubtle}/> - - - 0 - ? service?.metrics[0].total_high_urgency_incidents - : undefined} - label="high urgency" - color={theme.palette.warning.main}/> - - - 0 - ? service?.metrics[0].total_incident_count - : undefined} - label="incidents" - color={theme.palette.error.main}/> - - - + + + + + 0 + ? service?.metrics[0].total_interruptions + : undefined + } + label="interruptions" + color={theme.palette.textSubtle} + /> + + + 0 + ? service?.metrics[0].total_high_urgency_incidents + : undefined + } + label="high urgency" + color={theme.palette.warning.main} + /> + + + 0 + ? service?.metrics[0].total_incident_count + : undefined + } + label="incidents" + color={theme.palette.error.main} + /> + + + + + ) : ( + <> + )} + + {disableOnCall !== true ? ( + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + + ON CALL + + + + + + ) : ( <> )} - - - {disableOnCall !== true ? ( - - ) : ( - <> - )} - ); }; From 4788d4e7a4e91805ba00a7ddbebad829993b7c96 Mon Sep 17 00:00:00 2001 From: Tiago Barbosa Date: Tue, 30 Apr 2024 23:51:25 +0100 Subject: [PATCH 6/7] chore: remove commented code Signed-off-by: Tiago Barbosa --- src/components/Escalation/EscalationUser.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/components/Escalation/EscalationUser.tsx b/src/components/Escalation/EscalationUser.tsx index 06f2d1a..f0d5315 100644 --- a/src/components/Escalation/EscalationUser.tsx +++ b/src/components/Escalation/EscalationUser.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { ListItem, ListItemIcon, - // ListItemSecondaryAction, Tooltip, ListItemText, makeStyles, @@ -29,7 +28,6 @@ import Avatar from '@material-ui/core/Avatar'; import { PagerDutyUser } from '@pagerduty/backstage-plugin-common'; import NotificationsIcon from "@material-ui/icons/Notifications"; import { BackstageTheme } from '@backstage/theme'; -// import OpenInBrowser from '@material-ui/icons/OpenInBrowser'; const useStyles = makeStyles((theme) => ({ listItemPrimary: { @@ -136,18 +134,6 @@ export const EscalationUser = ({ user, policyUrl, policyName }: Props) => { } /> - {/* - - - - - - */} ); }; From 63d26fc522589400163fe3513151518c7005c3b1 Mon Sep 17 00:00:00 2001 From: Tiago Barbosa Date: Tue, 30 Apr 2024 23:51:59 +0100 Subject: [PATCH 7/7] style: adding support for compact styles for small card component Signed-off-by: Tiago Barbosa --- .../Escalation/EscalationPolicy.tsx | 13 ++++- .../PagerDutyCardCommon/InsightsCard.tsx | 5 +- .../PagerDutyCardCommon/OpenServiceButton.tsx | 52 ++++++++++--------- .../ServiceStandardsCard.tsx | 13 ++--- .../PagerDutyCardCommon/StatusCard.tsx | 5 +- .../TriggerIncidentButton.tsx | 47 +++++++++-------- src/components/PagerDutySmallCard/index.tsx | 15 ++++-- 7 files changed, 85 insertions(+), 65 deletions(-) diff --git a/src/components/Escalation/EscalationPolicy.tsx b/src/components/Escalation/EscalationPolicy.tsx index 0518eae..e2af10d 100644 --- a/src/components/Escalation/EscalationPolicy.tsx +++ b/src/components/Escalation/EscalationPolicy.tsx @@ -15,7 +15,7 @@ */ // eslint-disable-next-line @backstage/no-undeclared-imports import React from "react"; -import { List, ListSubheader } from "@material-ui/core"; +import { List, ListSubheader, createStyles, makeStyles } from "@material-ui/core"; import { EscalationUsersEmptyState } from "./EscalationUsersEmptyState"; import { EscalationUsersForbiddenState } from "./EscalationUsersForbiddenState"; import { EscalationUser } from "./EscalationUser"; @@ -25,12 +25,20 @@ import { Alert } from "@material-ui/lab"; import { useApi } from "@backstage/core-plugin-api"; import { Progress } from "@backstage/core-components"; +import { BackstageTheme } from "@backstage/theme"; type Props = { policyId: string; policyUrl: string; policyName: string; }; +const useStyles = makeStyles(() => + createStyles({ + listStyle: { + marginLeft: "-15px", + }, + }) +); export const EscalationPolicy = ({ policyId, @@ -38,6 +46,7 @@ export const EscalationPolicy = ({ policyName, }: Props) => { const api = useApi(pagerDutyApiRef); + const classes = useStyles(); const { value: users, @@ -74,7 +83,7 @@ export const EscalationPolicy = ({ return ( {users!.map((user, index) => ( (() => ({ cardStyle: { marginRight: "10px", - height: "120px", + height: compact !== true ? "120px" : "80px", display: "flex", alignItems: "center", justifyContent: "center", diff --git a/src/components/PagerDutyCardCommon/OpenServiceButton.tsx b/src/components/PagerDutyCardCommon/OpenServiceButton.tsx index 5b98b85..6b4b878 100644 --- a/src/components/PagerDutyCardCommon/OpenServiceButton.tsx +++ b/src/components/PagerDutyCardCommon/OpenServiceButton.tsx @@ -15,42 +15,44 @@ */ // eslint-disable-next-line @backstage/no-undeclared-imports -// import React, { useCallback, useState } from "react"; import React from "react"; import { makeStyles, IconButton } from "@material-ui/core"; import { BackstageTheme } from "@backstage/theme"; -// import { usePagerdutyEntity } from "../../hooks"; -// import { TriggerDialog } from "../TriggerDialog"; import OpenInBrowser from "@material-ui/icons/OpenInBrowser"; -const useStyles = makeStyles((theme) => ({ - buttonStyle: { - color: theme.palette.text.primary, - "&:hover": { - backgroundColor: "transparent", - textDecoration: "underline", - }, - }, - containerStyle: { - fontSize: "12px", - width: "85px", - }, - iconStyle: { - fontSize: "30px", - marginBottom: "-10px", - }, - textStyle: { - marginBottom: "-10px", - } -})); +type OpenServiceButtonProps = { + serviceUrl: string; + compact?: boolean; +}; /** @public */ -export function OpenServiceButton(props: { serviceUrl: string}) { +export function OpenServiceButton({ serviceUrl, compact }: OpenServiceButtonProps) { + const useStyles = makeStyles((theme) => ({ + buttonStyle: { + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "transparent", + textDecoration: "underline", + }, + }, + containerStyle: { + fontSize: compact !== true ? "12px" : "10px", + width: compact !== true ? "85px" : "70px", + }, + iconStyle: { + fontSize: "30px", + marginBottom: "-10px", + }, + textStyle: { + marginBottom: "-10px", + }, + })); + const { buttonStyle, containerStyle, iconStyle, textStyle } = useStyles(); function navigateToService() { - window.open(props.serviceUrl, "_blank"); + window.open(serviceUrl, "_blank"); } return ( diff --git a/src/components/PagerDutyCardCommon/ServiceStandardsCard.tsx b/src/components/PagerDutyCardCommon/ServiceStandardsCard.tsx index c975e16..5c786e9 100644 --- a/src/components/PagerDutyCardCommon/ServiceStandardsCard.tsx +++ b/src/components/PagerDutyCardCommon/ServiceStandardsCard.tsx @@ -19,6 +19,7 @@ type Props = { total: number | undefined; completed: number | undefined; standards: PagerDutyServiceStandard[] | undefined; + compact?: boolean; }; function colorFromPercentage(theme: Theme, percentage: number) { @@ -30,10 +31,10 @@ function colorFromPercentage(theme: Theme, percentage: number) { return theme.palette.success.main; } -function ServiceStandardsCard({ total, completed, standards }: Props) { +function ServiceStandardsCard({ total, completed, standards, compact }: Props) { const useStyles = makeStyles((theme) => ({ cardStyle: { - height: "120px", + height: compact !== true ? "120px" : "80px", display: "grid", gridTemplateRows: "1fr auto auto", backgroundColor: "rgba(0, 0, 0, 0.03)", @@ -41,10 +42,10 @@ function ServiceStandardsCard({ total, completed, standards }: Props) { containerStyle: { display: "flex", justifyContent: "center", - marginTop: "-100px", + marginTop: compact !== true ? "-100px" : "-50px", }, largeTextStyle: { - fontSize: "50px", + fontSize: compact !== true ? "50px" : "40px", color: completed !== undefined && total !== undefined ? colorFromPercentage(theme, completed / total) @@ -54,12 +55,12 @@ function ServiceStandardsCard({ total, completed, standards }: Props) { }, smallTextStyle: { color: theme.palette.textSubtle, - fontSize: "14px", + fontSize: compact !== true ? "14px" : "12px", fontWeight: "bold", alignSelf: "center", justifyContent: "center", marginLeft: "-2px", - marginTop: "25px", + marginTop: compact !== true ? "25px" : "20px", }, tooltipContainer: {}, tooltipIcon: { diff --git a/src/components/PagerDutyCardCommon/StatusCard.tsx b/src/components/PagerDutyCardCommon/StatusCard.tsx index 0056b44..c6700f2 100644 --- a/src/components/PagerDutyCardCommon/StatusCard.tsx +++ b/src/components/PagerDutyCardCommon/StatusCard.tsx @@ -11,6 +11,7 @@ import { Progress } from "@backstage/core-components"; type Props = { serviceId: string; refreshStatus: boolean; + compact?: boolean; }; function labelFromStatus(status: string) { @@ -65,7 +66,7 @@ function colorFromStatus(theme: Theme, status: string) { return color; } -function StatusCard({ serviceId, refreshStatus }: Props) { +function StatusCard({ serviceId, refreshStatus, compact}: Props) { const api = useApi(pagerDutyApiRef); const [{ value: status, loading, error }, getStatus] = useAsyncFn( async () => { @@ -76,7 +77,7 @@ function StatusCard({ serviceId, refreshStatus }: Props) { const useStyles = makeStyles((theme) => ({ cardStyle: { - height: "120px", + height: compact !== true ? "120px" : "80px", display: "flex", alignItems: "center", justifyContent: "center", diff --git a/src/components/PagerDutyCardCommon/TriggerIncidentButton.tsx b/src/components/PagerDutyCardCommon/TriggerIncidentButton.tsx index 1c00ce2..c799221 100644 --- a/src/components/PagerDutyCardCommon/TriggerIncidentButton.tsx +++ b/src/components/PagerDutyCardCommon/TriggerIncidentButton.tsx @@ -22,37 +22,38 @@ import { BackstageTheme } from "@backstage/theme"; import { TriggerDialog } from "../TriggerDialog"; import AddAlert from "@material-ui/icons/AddAlert"; -const useStyles = makeStyles((theme) => ({ - buttonStyle: { - color: theme.palette.text.primary, - "&:hover": { - backgroundColor: "transparent", - textDecoration: "underline", - }, - }, - containerStyle: { - fontSize: "12px", - width: "80px", - marginRight: "-10px", - }, - iconStyle: { - fontSize: "30px", - marginBottom: "-10px" - }, - textStyle: { - marginBottom: "-10px", - } -})); - /** @public */ export type TriggerIncidentButtonProps = { integrationKey: string | undefined; entityName: string; + compact?: boolean; handleRefresh: () => void; } /** @public */ -export function TriggerIncidentButton({ integrationKey, entityName, handleRefresh } : TriggerIncidentButtonProps) { +export function TriggerIncidentButton({ integrationKey, entityName, compact, handleRefresh } : TriggerIncidentButtonProps) { + const useStyles = makeStyles((theme) => ({ + buttonStyle: { + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "transparent", + textDecoration: "underline", + }, + }, + containerStyle: { + fontSize: compact !== true ? "12px" : "10px", + width: compact !== true ? "80px" : "60px", + marginRight: "-10px", + }, + iconStyle: { + fontSize: "30px", + marginBottom: "-10px", + }, + textStyle: { + marginBottom: "-10px", + }, + })); + const { buttonStyle, containerStyle, iconStyle, textStyle } = useStyles(); const [dialogShown, setDialogShown] = useState(false); diff --git a/src/components/PagerDutySmallCard/index.tsx b/src/components/PagerDutySmallCard/index.tsx index 3d720e8..9fc9012 100644 --- a/src/components/PagerDutySmallCard/index.tsx +++ b/src/components/PagerDutySmallCard/index.tsx @@ -187,24 +187,25 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { className={classes.headerStyle} title={ theme.palette.type === "dark" ? ( - PagerDuty + PagerDuty ) : ( - PagerDuty + PagerDuty ) } action={ !readOnly && props.integrationKey ? (
- +
) : ( - + ) } /> @@ -223,10 +224,11 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { - + { > 0 ? service?.metrics[0].total_interruptions @@ -280,6 +283,7 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { 0 ? service?.metrics[0].total_high_urgency_incidents @@ -291,6 +295,7 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { 0