@incident-io/backstage 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +22 -3
  2. package/config.d.ts +11 -5
  3. package/dist/alpha.esm.js +32 -6
  4. package/dist/alpha.esm.js.map +1 -1
  5. package/dist/{esm/client-646572ea.esm.js → api/client.esm.js} +6 -13
  6. package/dist/api/client.esm.js.map +1 -0
  7. package/dist/components/AlertListItem/index.esm.js +68 -0
  8. package/dist/components/AlertListItem/index.esm.js.map +1 -0
  9. package/dist/components/EntityAlertCard/index.esm.js +113 -0
  10. package/dist/components/EntityAlertCard/index.esm.js.map +1 -0
  11. package/dist/components/EntityIncidentCard/index.esm.js +125 -0
  12. package/dist/components/EntityIncidentCard/index.esm.js.map +1 -0
  13. package/dist/components/EntityOnCallCard/index.esm.js +198 -0
  14. package/dist/components/EntityOnCallCard/index.esm.js.map +1 -0
  15. package/dist/components/HomePageAlertCard/Content.esm.js +76 -0
  16. package/dist/components/HomePageAlertCard/Content.esm.js.map +1 -0
  17. package/dist/components/HomePageAlertCard/index.esm.js +2 -0
  18. package/dist/components/HomePageAlertCard/index.esm.js.map +1 -0
  19. package/dist/components/HomePageIncidentCard/Content.esm.js +54 -0
  20. package/dist/components/HomePageIncidentCard/Content.esm.js.map +1 -0
  21. package/dist/components/HomePageIncidentCard/Context.esm.js +33 -0
  22. package/dist/components/HomePageIncidentCard/Context.esm.js.map +1 -0
  23. package/dist/components/HomePageIncidentCard/index.esm.js +3 -0
  24. package/dist/components/HomePageIncidentCard/index.esm.js.map +1 -0
  25. package/dist/components/HomePageOnCallCard/Content.esm.js +38 -0
  26. package/dist/components/HomePageOnCallCard/Content.esm.js.map +1 -0
  27. package/dist/components/HomePageOnCallCard/index.esm.js +6 -0
  28. package/dist/components/HomePageOnCallCard/index.esm.js.map +1 -0
  29. package/dist/components/IncidentListItem/index.esm.js +68 -0
  30. package/dist/components/IncidentListItem/index.esm.js.map +1 -0
  31. package/dist/components/styles.esm.js +34 -0
  32. package/dist/components/styles.esm.js.map +1 -0
  33. package/dist/components/utils.esm.js +19 -0
  34. package/dist/components/utils.esm.js.map +1 -0
  35. package/dist/hooks/useIncidentRequest.esm.js +65 -0
  36. package/dist/hooks/useIncidentRequest.esm.js.map +1 -0
  37. package/dist/hooks/useOnCallRequest.esm.js +116 -0
  38. package/dist/hooks/useOnCallRequest.esm.js.map +1 -0
  39. package/dist/index.d.ts +9 -6
  40. package/dist/index.esm.js +1 -64
  41. package/dist/index.esm.js.map +1 -1
  42. package/dist/plugin.esm.js +99 -0
  43. package/dist/plugin.esm.js.map +1 -0
  44. package/package.json +44 -23
  45. package/src/alpha.test.ts +9 -0
  46. package/src/alpha.tsx +38 -4
  47. package/src/api/client.test.ts +43 -0
  48. package/src/api/types.test.ts +15 -0
  49. package/src/api/types.ts +49796 -11325
  50. package/src/components/AlertListItem/index.tsx +82 -0
  51. package/src/components/EntityAlertCard/index.test.tsx +242 -0
  52. package/src/components/EntityAlertCard/index.tsx +168 -0
  53. package/src/components/EntityIncidentCard/index.test.tsx +135 -0
  54. package/src/components/EntityIncidentCard/index.tsx +3 -23
  55. package/src/components/EntityOnCallCard/index.test.tsx +134 -0
  56. package/src/components/EntityOnCallCard/index.tsx +301 -0
  57. package/src/components/HomePageAlertCard/Content.test.tsx +56 -0
  58. package/src/components/HomePageAlertCard/Content.tsx +85 -0
  59. package/src/components/HomePageAlertCard/index.tsx +1 -0
  60. package/src/components/HomePageIncidentCard/Content.test.tsx +4 -3
  61. package/src/components/HomePageIncidentCard/Content.tsx +2 -2
  62. package/src/components/HomePageIncidentCard/Context.tsx +2 -2
  63. package/src/components/HomePageOnCallCard/Content.test.tsx +90 -0
  64. package/src/components/HomePageOnCallCard/Content.tsx +58 -0
  65. package/src/components/HomePageOnCallCard/index.ts +3 -0
  66. package/src/components/IncidentListItem/index.tsx +3 -26
  67. package/src/components/styles.tsx +30 -0
  68. package/src/components/utils.tsx +24 -0
  69. package/src/hooks/useIncidentRequest.test.ts +189 -0
  70. package/src/hooks/useIncidentRequest.ts +56 -3
  71. package/src/hooks/useOnCallRequest.test.ts +52 -0
  72. package/src/hooks/useOnCallRequest.ts +141 -0
  73. package/src/index.ts +4 -0
  74. package/src/plugin.ts +45 -1
  75. package/src/setupTests.ts +2 -2
  76. package/alpha/package.json +0 -7
  77. package/dist/esm/client-646572ea.esm.js.map +0 -1
  78. package/dist/esm/index-55bf4982.esm.js +0 -72
  79. package/dist/esm/index-55bf4982.esm.js.map +0 -1
  80. package/dist/esm/index-633a0241.esm.js +0 -96
  81. package/dist/esm/index-633a0241.esm.js.map +0 -1
  82. package/dist/esm/index-a220a8f7.esm.js +0 -116
  83. package/dist/esm/index-a220a8f7.esm.js.map +0 -1
@@ -0,0 +1,90 @@
1
+ /// <reference types="@testing-library/jest-dom" />
2
+ import { screen } from "@testing-library/react";
3
+ import { renderInTestApp } from "@backstage/test-utils";
4
+ import { vi } from "vitest";
5
+
6
+ vi.mock("../../hooks/useIncidentRequest", () => ({
7
+ useIdentity: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("../../hooks/useOnCallRequest", () => ({
11
+ useAllEscalationPaths: vi.fn(),
12
+ useAllSchedules: vi.fn(),
13
+ }));
14
+
15
+ vi.mock("@backstage/core-components", () => ({
16
+ Progress: () => <div data-testid="progress" />,
17
+ }));
18
+
19
+ import { useIdentity } from "../../hooks/useIncidentRequest";
20
+ import { useAllEscalationPaths, useAllSchedules } from "../../hooks/useOnCallRequest";
21
+ import { Content } from "./Content";
22
+
23
+ const mockIdentityLoaded = {
24
+ value: { identity: { dashboard_url: "https://app.incident.io" } },
25
+ loading: false,
26
+ error: undefined,
27
+ };
28
+
29
+ beforeEach(() => {
30
+ (useIdentity as ReturnType<typeof vi.fn>).mockReturnValue(mockIdentityLoaded);
31
+ });
32
+
33
+ describe("HomePageOnCallCard Content", () => {
34
+ it("should show a loading indicator while fetching", async () => {
35
+ (useAllEscalationPaths as ReturnType<typeof vi.fn>).mockReturnValue({
36
+ value: undefined,
37
+ loading: true,
38
+ error: undefined,
39
+ });
40
+ (useAllSchedules as ReturnType<typeof vi.fn>).mockReturnValue({
41
+ value: undefined,
42
+ loading: true,
43
+ error: undefined,
44
+ });
45
+
46
+ await renderInTestApp(<Content />);
47
+
48
+ expect(screen.getByTestId("progress")).toBeInTheDocument();
49
+ });
50
+
51
+ it("should show empty states when there are no EPs or schedules", async () => {
52
+ (useAllEscalationPaths as ReturnType<typeof vi.fn>).mockReturnValue({
53
+ value: [],
54
+ loading: false,
55
+ error: undefined,
56
+ });
57
+ (useAllSchedules as ReturnType<typeof vi.fn>).mockReturnValue({
58
+ value: [],
59
+ loading: false,
60
+ error: undefined,
61
+ });
62
+
63
+ await renderInTestApp(<Content />);
64
+
65
+ expect(screen.getByText(/No escalation paths/i)).toBeInTheDocument();
66
+ expect(screen.getByText(/No schedules/i)).toBeInTheDocument();
67
+ });
68
+
69
+ it("should render EP and schedule names when data is loaded", async () => {
70
+ (useAllEscalationPaths as ReturnType<typeof vi.fn>).mockReturnValue({
71
+ value: [
72
+ { id: "ep-1", name: "Primary EP" },
73
+ { id: "ep-2", name: "Secondary EP" },
74
+ ],
75
+ loading: false,
76
+ error: undefined,
77
+ });
78
+ (useAllSchedules as ReturnType<typeof vi.fn>).mockReturnValue({
79
+ value: [{ id: "sched-1", name: "Primary Schedule" }],
80
+ loading: false,
81
+ error: undefined,
82
+ });
83
+
84
+ await renderInTestApp(<Content />);
85
+
86
+ expect(screen.getByText("Primary EP")).toBeInTheDocument();
87
+ expect(screen.getByText("Secondary EP")).toBeInTheDocument();
88
+ expect(screen.getByText("Primary Schedule")).toBeInTheDocument();
89
+ });
90
+ });
@@ -0,0 +1,58 @@
1
+ import { Progress } from "@backstage/core-components";
2
+ import { useIdentity } from "../../hooks/useIncidentRequest";
3
+ import { useAllEscalationPaths, useAllSchedules } from "../../hooks/useOnCallRequest";
4
+ import { Alert } from "@material-ui/lab";
5
+ import {
6
+ Box,
7
+ Divider,
8
+ IconButton,
9
+ Tooltip,
10
+ Typography,
11
+ } from "@material-ui/core";
12
+ import OpenInBrowserIcon from "@material-ui/icons/OpenInBrowser";
13
+
14
+ export const Content = () => {
15
+ const { value: identity } = useIdentity();
16
+ const baseUrl = identity?.identity.dashboard_url ?? "app.incident.io";
17
+
18
+ const { value: eps, loading: epsLoading, error: epsError } = useAllEscalationPaths();
19
+ const { value: schedules, loading: schedulesLoading, error: schedulesError } = useAllSchedules();
20
+
21
+ if (epsLoading || schedulesLoading) return <Progress />;
22
+
23
+ return (
24
+ <>
25
+ <Typography variant="subtitle1"><strong>Escalation Paths</strong></Typography>
26
+ <Divider />
27
+ {epsError && <Alert severity="error">{epsError.message}</Alert>}
28
+ {eps && eps.length === 0 && <Typography variant="body2" color="textSecondary">No escalation paths.</Typography>}
29
+ {eps && eps.map(ep => (
30
+ <Box key={ep.id} display="flex" alignItems="center" justifyContent="space-between" py={0.5}>
31
+ <Typography variant="body2">{ep.name}</Typography>
32
+ <Tooltip title="View in incident.io" placement="top">
33
+ <IconButton size="small" href={`${baseUrl}/on-call/escalation-paths/${ep.id}`} target="_blank" rel="noopener noreferrer" color="primary">
34
+ <OpenInBrowserIcon fontSize="small" />
35
+ </IconButton>
36
+ </Tooltip>
37
+ </Box>
38
+ ))}
39
+
40
+ <Box mt={2}>
41
+ <Typography variant="subtitle1"><strong>Schedules</strong></Typography>
42
+ <Divider />
43
+ {schedulesError && <Alert severity="error">{schedulesError.message}</Alert>}
44
+ {schedules && schedules.length === 0 && <Typography variant="body2" color="textSecondary">No schedules.</Typography>}
45
+ {schedules && schedules.map(schedule => (
46
+ <Box key={schedule.id} display="flex" alignItems="center" justifyContent="space-between" py={0.5}>
47
+ <Typography variant="body2">{schedule.name}</Typography>
48
+ <Tooltip title="View in incident.io" placement="top">
49
+ <IconButton size="small" href={`${baseUrl}/on-call/schedules/${schedule.id}`} target="_blank" rel="noopener noreferrer" color="primary">
50
+ <OpenInBrowserIcon fontSize="small" />
51
+ </IconButton>
52
+ </Tooltip>
53
+ </Box>
54
+ ))}
55
+ </Box>
56
+ </>
57
+ );
58
+ };
@@ -0,0 +1,3 @@
1
+ export { Content } from "./Content";
2
+
3
+ export const ContextProvider = ({ children }: { children: JSX.Element }) => children;
@@ -14,7 +14,6 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { DateTime, Duration } from "luxon";
17
- import { BackstageTheme } from "@backstage/theme";
18
17
  import {
19
18
  Chip,
20
19
  IconButton,
@@ -23,32 +22,10 @@ import {
23
22
  ListItemText,
24
23
  Tooltip,
25
24
  Typography,
26
- makeStyles,
27
25
  } from "@material-ui/core";
28
26
  import OpenInBrowserIcon from "@material-ui/icons/OpenInBrowser";
29
- import React from "react";
30
- import { definitions } from "../../api/types";
31
-
32
- const useStyles = makeStyles<BackstageTheme>((theme) => ({
33
- listItemPrimary: {
34
- display: "flex", // vertically align with chip
35
- fontWeight: "bold",
36
- },
37
- warning: {
38
- borderColor: theme.palette.status.warning,
39
- color: theme.palette.status.warning,
40
- "& *": {
41
- color: theme.palette.status.warning,
42
- },
43
- },
44
- error: {
45
- borderColor: theme.palette.status.error,
46
- color: theme.palette.status.error,
47
- "& *": {
48
- color: theme.palette.status.error,
49
- },
50
- },
51
- }));
27
+ import { components } from "../../api/types";
28
+ import {useStyles} from "../styles";
52
29
 
53
30
  // Single item in the list of on-going incidents.
54
31
  export const IncidentListItem = ({
@@ -56,7 +33,7 @@ export const IncidentListItem = ({
56
33
  incident,
57
34
  }: {
58
35
  baseUrl: string;
59
- incident: definitions["IncidentV2ResponseBody"];
36
+ incident: components["schemas"]["IncidentV2"];
60
37
  }) => {
61
38
  const classes = useStyles();
62
39
  const reportedAt = incident.incident_timestamp_values?.find((ts) =>
@@ -0,0 +1,30 @@
1
+ import { makeStyles, Theme } from "@material-ui/core";
2
+
3
+ export const useStyles = makeStyles<Theme>((theme) => ({
4
+ listItemPrimary: {
5
+ display: "flex", // vertically align with chip
6
+ fontWeight: "bold",
7
+ gap: theme.spacing(1),
8
+ },
9
+ warning: {
10
+ borderColor: theme.palette.warning.main,
11
+ color: theme.palette.warning.main,
12
+ "& *": {
13
+ color: theme.palette.warning.main,
14
+ },
15
+ },
16
+ error: {
17
+ borderColor: theme.palette.error.main,
18
+ color: theme.palette.error.main,
19
+ "& *": {
20
+ color: theme.palette.error.main,
21
+ },
22
+ },
23
+ success: {
24
+ borderColor: theme.palette.success.main,
25
+ color: theme.palette.success.main,
26
+ "& *": {
27
+ color: theme.palette.success.main,
28
+ },
29
+ },
30
+ }));
@@ -0,0 +1,24 @@
1
+ import { Entity } from "@backstage/catalog-model";
2
+ import { ConfigApi } from "@backstage/core-plugin-api";
3
+
4
+
5
+ // Find the ID of the custom field in incident that represents the association
6
+ // to this type of entity.
7
+ //
8
+ // In practice, this will be kind=Component => ID of Affected components field.
9
+ export function getEntityFieldID(config: ConfigApi, entity: Entity) {
10
+ switch (entity.kind) {
11
+ case "API":
12
+ return config.getOptional("incident.fields.api");
13
+ case "Component":
14
+ return config.getOptional("incident.fields.component");
15
+ case "Domain":
16
+ return config.getOptional("incident.fields.domain");
17
+ case "System":
18
+ return config.getOptional("incident.fields.system");
19
+ case "Group":
20
+ return config.getOptional("incident.fields.group");
21
+ default:
22
+ throw new Error(`unrecognised entity kind: ${entity.kind}`);
23
+ }
24
+ }
@@ -0,0 +1,189 @@
1
+ import { renderHook, waitFor, act } from "@testing-library/react";
2
+ import { vi } from "vitest";
3
+ import {
4
+ useIncidentList,
5
+ useIdentity,
6
+ useAlertList,
7
+ useIncidentAlertList,
8
+ useAlertSourceList,
9
+ } from "./useIncidentRequest";
10
+
11
+ // Mock @backstage/core-plugin-api so useApi returns our fake client
12
+ vi.mock("@backstage/core-plugin-api", () => ({
13
+ useApi: vi.fn(),
14
+ createApiRef: vi.fn(),
15
+ }));
16
+
17
+ import { useApi } from "@backstage/core-plugin-api";
18
+
19
+ describe("useIncidentList", () => {
20
+ it("should return incidents from the API", async () => {
21
+ const mockResponse = { incidents: [{ id: "INC-1", name: "Test incident" }] };
22
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({
23
+ request: vi.fn().mockResolvedValue(mockResponse),
24
+ });
25
+
26
+ const { result } = renderHook(() =>
27
+ useIncidentList(new URLSearchParams({ status: "active" }), []),
28
+ );
29
+
30
+ await waitFor(() => expect(result.current.loading).toBe(false));
31
+
32
+ expect(result.current.value).toEqual(mockResponse);
33
+ expect(result.current.error).toBeUndefined();
34
+ });
35
+ });
36
+
37
+ describe("useAlertList", () => {
38
+ const mockAlert = {
39
+ id: "01KNS58MGQGMHDT8C2X8094MAF",
40
+ title: "High error rate",
41
+ description: "CPU exceeded 75% for 5 minutes",
42
+ status: "firing",
43
+ created_at: "2026-04-09T12:00:00Z",
44
+ updated_at: "2026-04-09T12:00:00Z",
45
+ deduplication_key: "abc123",
46
+ alert_source_id: "src-1",
47
+ source_url: "https://datadog.com/alerts/123",
48
+ };
49
+ const mockResponse = { alerts: [mockAlert], pagination_meta: { page_size: 25 } };
50
+
51
+ it("should return alerts from the API", async () => {
52
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({
53
+ request: vi.fn().mockResolvedValue(mockResponse),
54
+ });
55
+
56
+ const { result } = renderHook(() => useAlertList());
57
+
58
+ await waitFor(() => expect(result.current.loading).toBe(false));
59
+
60
+ expect(result.current.value).toEqual(mockResponse);
61
+ expect(result.current.error).toBeUndefined();
62
+ });
63
+
64
+ it("passes the status filter as a query param", async () => {
65
+ const mockRequest = vi.fn().mockResolvedValue(mockResponse);
66
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({ request: mockRequest });
67
+
68
+ const { result } = renderHook(() => useAlertList("firing"));
69
+
70
+ await waitFor(() => expect(result.current.loading).toBe(false));
71
+
72
+ expect(mockRequest).toHaveBeenCalledWith(
73
+ expect.objectContaining({ path: expect.stringContaining("status%5Bone_of%5D=firing") }),
74
+ );
75
+ });
76
+
77
+ it("does not pass a status param when status is undefined", async () => {
78
+ const mockRequest = vi.fn().mockResolvedValue(mockResponse);
79
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({ request: mockRequest });
80
+
81
+ const { result } = renderHook(() => useAlertList(undefined));
82
+
83
+ await waitFor(() => expect(result.current.loading).toBe(false));
84
+
85
+ expect(mockRequest).toHaveBeenCalledWith(
86
+ expect.objectContaining({ path: expect.not.stringContaining("status") }),
87
+ );
88
+ });
89
+
90
+ it("re-fetches when deps change", async () => {
91
+ const mockRequest = vi.fn().mockResolvedValue(mockResponse);
92
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({ request: mockRequest });
93
+
94
+ let reload = false;
95
+ const { result, rerender } = renderHook(() => useAlertList("firing", [reload]));
96
+
97
+ await waitFor(() => expect(result.current.loading).toBe(false));
98
+ expect(mockRequest).toHaveBeenCalledTimes(1);
99
+
100
+ act(() => { reload = true; });
101
+ rerender();
102
+
103
+ await waitFor(() => expect(mockRequest).toHaveBeenCalledTimes(2));
104
+ });
105
+ });
106
+
107
+ describe("useIncidentAlertList", () => {
108
+ it("returns empty immediately when no incident IDs are given", async () => {
109
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({
110
+ request: vi.fn(),
111
+ });
112
+
113
+ const { result } = renderHook(() => useIncidentAlertList([]));
114
+
115
+ await waitFor(() => expect(result.current.loading).toBe(false));
116
+
117
+ expect(result.current.value?.incident_alerts).toEqual([]);
118
+ expect(result.current.error).toBeUndefined();
119
+ });
120
+
121
+ it("makes one request per incident ID and flattens results", async () => {
122
+ const mockRequest = vi.fn()
123
+ .mockResolvedValueOnce({ incident_alerts: [{ id: "ia-1", alert: { id: "a-1" }, incident: { id: "inc-1" } }], pagination_meta: { page_size: 25 } })
124
+ .mockResolvedValueOnce({ incident_alerts: [{ id: "ia-2", alert: { id: "a-2" }, incident: { id: "inc-2" } }], pagination_meta: { page_size: 25 } });
125
+
126
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({ request: mockRequest });
127
+
128
+ const { result } = renderHook(() => useIncidentAlertList(["inc-1", "inc-2"]));
129
+
130
+ await waitFor(() => expect(result.current.loading).toBe(false));
131
+
132
+ expect(mockRequest).toHaveBeenCalledTimes(2);
133
+ expect(result.current.value?.incident_alerts).toHaveLength(2);
134
+ expect(result.current.value?.incident_alerts.map(ia => ia.id)).toEqual(["ia-1", "ia-2"]);
135
+ });
136
+
137
+ it("re-fetches when deps change even if incident IDs are unchanged", async () => {
138
+ const mockRequest = vi.fn().mockResolvedValue({
139
+ incident_alerts: [{ id: "ia-1", alert: { id: "a-1" }, incident: { id: "inc-1" } }],
140
+ pagination_meta: { page_size: 25 },
141
+ });
142
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({ request: mockRequest });
143
+
144
+ let reload = false;
145
+ const { result, rerender } = renderHook(() => useIncidentAlertList(["inc-1"], [reload]));
146
+
147
+ await waitFor(() => expect(result.current.loading).toBe(false));
148
+ expect(mockRequest).toHaveBeenCalledTimes(1);
149
+
150
+ act(() => { reload = true; });
151
+ rerender();
152
+
153
+ await waitFor(() => expect(mockRequest).toHaveBeenCalledTimes(2));
154
+ });
155
+ });
156
+
157
+ describe("useAlertSourceList", () => {
158
+ it("should return alert sources from the API", async () => {
159
+ const mockResponse = {
160
+ alert_sources: [{ id: "src-1", name: "Datadog", source_type: "datadog" }],
161
+ };
162
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({
163
+ request: vi.fn().mockResolvedValue(mockResponse),
164
+ });
165
+
166
+ const { result } = renderHook(() => useAlertSourceList());
167
+
168
+ await waitFor(() => expect(result.current.loading).toBe(false));
169
+
170
+ expect(result.current.value).toEqual(mockResponse);
171
+ expect(result.current.error).toBeUndefined();
172
+ });
173
+ });
174
+
175
+ describe("useIdentity", () => {
176
+ it("should return identity from the API", async () => {
177
+ const mockIdentity = { current_user: { id: "user-1", name: "Alice" } };
178
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({
179
+ request: vi.fn().mockResolvedValue(mockIdentity),
180
+ });
181
+
182
+ const { result } = renderHook(() => useIdentity());
183
+
184
+ await waitFor(() => expect(result.current.loading).toBe(false));
185
+
186
+ expect(result.current.value).toEqual(mockIdentity);
187
+ expect(result.current.error).toBeUndefined();
188
+ });
189
+ });
@@ -1,7 +1,7 @@
1
1
  import { useApi } from "@backstage/core-plugin-api";
2
2
  import { useAsync } from "react-use";
3
3
  import { IncidentApiRef } from "../api/client";
4
- import { definitions } from "../api/types";
4
+ import { components } from "../api/types";
5
5
  import { DependencyList } from "react";
6
6
 
7
7
  export const useIncidentList = (
@@ -12,7 +12,7 @@ export const useIncidentList = (
12
12
 
13
13
  const { value, loading, error } = useAsync(async () => {
14
14
  return await IncidentApi.request<
15
- definitions["IncidentsV2ListResponseBody"]
15
+ components["schemas"]["IncidentsListResultV2"]
16
16
  >({
17
17
  path: `/v2/incidents?${query.toString()}`,
18
18
  });
@@ -21,12 +21,65 @@ export const useIncidentList = (
21
21
  return { loading, error, value };
22
22
  };
23
23
 
24
+ export const useAlertList = (status?: "firing" | "resolved", deps?: DependencyList) => {
25
+ const IncidentApi = useApi(IncidentApiRef);
26
+
27
+ const { value, loading, error } = useAsync(async () => {
28
+ const query = new URLSearchParams({ page_size: "25" });
29
+ if (status) query.set("status[one_of]", status);
30
+ return await IncidentApi.request<
31
+ components["schemas"]["AlertsListResultV2"]
32
+ >({
33
+ path: `/v2/alerts?${query.toString()}`,
34
+ });
35
+ }, [status, ...(deps ?? [])]);
36
+
37
+ return { loading, error, value };
38
+ };
39
+
40
+ export const useAlertSourceList = () => {
41
+ const IncidentApi = useApi(IncidentApiRef);
42
+
43
+ const { value, loading, error } = useAsync(async () => {
44
+ return await IncidentApi.request<
45
+ components["schemas"]["AlertSourcesListResultV2"]
46
+ >({
47
+ path: `/v2/alert_sources`,
48
+ });
49
+ });
50
+
51
+ return { loading, error, value };
52
+ };
53
+
54
+ export const useIncidentAlertList = (incidentIds: string[], deps?: DependencyList) => {
55
+ const IncidentApi = useApi(IncidentApiRef);
56
+
57
+ const { value, loading, error } = useAsync(async () => {
58
+ if (incidentIds.length === 0) {
59
+ return { incident_alerts: [], pagination_meta: { page_size: 25 } };
60
+ }
61
+ const results = await Promise.all(
62
+ incidentIds.map(id =>
63
+ IncidentApi.request<components["schemas"]["AlertsListIncidentAlertsResultV2"]>({
64
+ path: `/v2/incident_alerts?incident_id=${id}&page_size=25`,
65
+ }),
66
+ ),
67
+ );
68
+ return {
69
+ incident_alerts: results.flatMap(r => r.incident_alerts),
70
+ pagination_meta: { page_size: 25 },
71
+ };
72
+ }, [incidentIds.join(","), ...(deps ?? [])]);
73
+
74
+ return { loading, error, value };
75
+ };
76
+
24
77
  export const useIdentity = () => {
25
78
  const IncidentApi = useApi(IncidentApiRef);
26
79
 
27
80
  const { value, loading, error } = useAsync(async () => {
28
81
  return await IncidentApi.request<
29
- definitions["UtilitiesV1IdentityResponseBody"]
82
+ components["schemas"]["UtilitiesIdentityResultV1"]
30
83
  >({
31
84
  path: `/v1/identity`,
32
85
  });
@@ -0,0 +1,52 @@
1
+ import { renderHook, waitFor } from "@testing-library/react";
2
+ import { vi } from "vitest";
3
+ import { useAllEscalationPaths, useAllSchedules } from "./useOnCallRequest";
4
+
5
+ vi.mock("@backstage/core-plugin-api", () => ({
6
+ useApi: vi.fn(),
7
+ createApiRef: vi.fn(),
8
+ }));
9
+
10
+ import { useApi } from "@backstage/core-plugin-api";
11
+
12
+ describe("useAllEscalationPaths", () => {
13
+ it("should return all escalation paths from the API", async () => {
14
+ const mockResponse = {
15
+ escalation_paths: [
16
+ { id: "ep-1", name: "Primary EP" },
17
+ { id: "ep-2", name: "Secondary EP" },
18
+ ],
19
+ };
20
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({
21
+ request: vi.fn().mockResolvedValue(mockResponse),
22
+ });
23
+
24
+ const { result } = renderHook(() => useAllEscalationPaths([]));
25
+
26
+ await waitFor(() => expect(result.current.loading).toBe(false));
27
+
28
+ expect(result.current.value).toEqual(mockResponse.escalation_paths);
29
+ expect(result.current.error).toBeUndefined();
30
+ });
31
+ });
32
+
33
+ describe("useAllSchedules", () => {
34
+ it("should return all schedules from the API", async () => {
35
+ const mockResponse = {
36
+ schedules: [
37
+ { id: "sched-1", name: "Primary Schedule" },
38
+ { id: "sched-2", name: "Secondary Schedule" },
39
+ ],
40
+ };
41
+ (useApi as ReturnType<typeof vi.fn>).mockReturnValue({
42
+ request: vi.fn().mockResolvedValue(mockResponse),
43
+ });
44
+
45
+ const { result } = renderHook(() => useAllSchedules([]));
46
+
47
+ await waitFor(() => expect(result.current.loading).toBe(false));
48
+
49
+ expect(result.current.value).toEqual(mockResponse.schedules);
50
+ expect(result.current.error).toBeUndefined();
51
+ });
52
+ });