diff --git a/.changeset/spicy-elephants-decide.md b/.changeset/spicy-elephants-decide.md
new file mode 100644
index 0000000000..2444a60b05
--- /dev/null
+++ b/.changeset/spicy-elephants-decide.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-splunk-on-call': patch
+---
+
+The Splunk On-Call plugin now supports an optional `readOnly` property (``) for suppressing the rendering of incident trigger-acknowledge-resolve controls from the Backstage UI.
diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md
index 0200cacc50..5078dfa38b 100644
--- a/plugins/splunk-on-call/README.md
+++ b/plugins/splunk-on-call/README.md
@@ -45,6 +45,14 @@ const overviewContent = (
```
+### `readOnly` mode
+
+To suppress the rendering of the actionable create-acknowledge-resolve incident buttons and UI controls, the `EntitySplunkOnCallCard` can also be instantiated in `readOnly` mode:
+
+```ts
+
+```
+
## Client configuration
In order to be able to perform certain actions (create-acknowledge-resolve an action), you need to provide a REST Endpoint.
@@ -74,6 +82,19 @@ proxy:
In addition, to make certain API calls (trigger-resolve-acknowledge an incident) you need to add the `PATCH` method to the backend `cors` methods list: `[GET, POST, PUT, DELETE, PATCH]`.
+**WARNING**: In current implementation, the Splunk OnCall plugin requires the `/splunk-on-call` proxy endpoint be exposed by the Backstage backend as an unprotected endpoint, in effect enabling Splunk OnCall API access using the configured `SPLUNK_ON_CALL_API_KEY` for any user or process with access to the `/splunk-on-call` Backstage backend endpoint. See below for further configuration options enabling protection of this endpoint. If you regard this as problematic, consider using the plugin in `readOnly` mode (``) using the following proxy configuration:
+
+```yaml
+proxy:
+ '/splunk-on-call':
+ target: https://api.victorops.com/api-public
+ headers:
+ X-VO-Api-Id: ${SPLUNK_ON_CALL_API_ID}
+ X-VO-Api-Key: ${SPLUNK_ON_CALL_API_KEY}
+ # prohibit the `/splunk-on-call` proxy endpoint from servicing non-GET requests
+ allowedMethods: ['GET']
+```
+
### Adding your team name to the entity annotation
The information displayed for each entity is based on either an associated team name or an associated routing key.
diff --git a/plugins/splunk-on-call/api-report.md b/plugins/splunk-on-call/api-report.md
index 8e8076d9e8..60ec38ca6d 100644
--- a/plugins/splunk-on-call/api-report.md
+++ b/plugins/splunk-on-call/api-report.md
@@ -12,10 +12,13 @@ import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { RouteRef } from '@backstage/core-plugin-api';
+// Warning: (ae-forgotten-export) The symbol "EntitySplunkOnCallCardProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "EntitySplunkOnCallCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export const EntitySplunkOnCallCard: () => JSX.Element;
+export const EntitySplunkOnCallCard: (
+ props: EntitySplunkOnCallCardProps,
+) => JSX.Element;
// Warning: (ae-missing-release-tag) "isSplunkOnCallAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx
index a83db136e4..0688fb8237 100644
--- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx
+++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx
@@ -131,6 +131,32 @@ describe('SplunkOnCallCard', () => {
expect(getByText('Empty escalation policy')).toBeInTheDocument();
});
+ it('does not render a "Create incident" link in read only mode', async () => {
+ mockSplunkOnCallApi.getUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCKED_USER]);
+ mockSplunkOnCallApi.getTeams = jest
+ .fn()
+ .mockImplementation(async () => [MOCK_TEAM_NO_INCIDENTS]);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(() => getByText('Create Incident')).toThrow();
+ await waitFor(
+ () => expect(getByText('Nice! No incidents found!')).toBeInTheDocument(),
+ { timeout: 2000 },
+ );
+ expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ });
+
it('handles a "splunk.com/on-call-routing-key" annotation', async () => {
mockSplunkOnCallApi.getUsers = jest
.fn()
diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx
index 4a6f983636..4f6bcd9a0e 100644
--- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx
+++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx
@@ -109,7 +109,14 @@ const useStyles = makeStyles({
},
});
-export const EntitySplunkOnCallCard = () => {
+/** @public */
+export type EntitySplunkOnCallCardProps = {
+ readOnly?: boolean;
+};
+
+/** @public */
+export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => {
+ const { readOnly } = props;
const classes = useStyles();
const config = useApi(configApiRef);
const api = useApi(splunkOnCallApiRef);
@@ -218,7 +225,11 @@ export const EntitySplunkOnCallCard = () => {
return (
<>
-
+
{usersHashMap && team && (
)}
@@ -259,7 +270,7 @@ export const EntitySplunkOnCallCard = () => {
,
,
]}
/>
diff --git a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx
index 4c9975bd06..d8b6a56efc 100644
--- a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx
+++ b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx
@@ -64,6 +64,7 @@ type Props = {
team: string;
incident: Incident;
onIncidentAction: () => void;
+ readOnly: boolean;
};
const IncidentPhaseStatus = ({
@@ -135,6 +136,7 @@ const IncidentAction = ({
export const IncidentListItem = ({
incident,
+ readOnly,
onIncidentAction,
team,
}: Props) => {
@@ -241,12 +243,14 @@ export const IncidentListItem = ({
{incident.incidentLink && incident.incidentNumber && (
-
+ {!readOnly && (
+
+ )}
{
const { getByText, queryByTestId } = render(
wrapInTestApp(
-
+
,
),
);
@@ -68,7 +68,7 @@ describe('Incidents', () => {
} = render(
wrapInTestApp(
-
+
,
),
);
@@ -90,6 +90,40 @@ describe('Incidents', () => {
expect(getAllByTitle('View in Splunk On-Call').length).toEqual(1);
});
+ it('does not render incident action buttons in read only mode', async () => {
+ mockSplunkOnCallApi.getIncidents.mockResolvedValue([MOCK_INCIDENT]);
+ mockSplunkOnCallApi.getTeams.mockResolvedValue([MOCK_TEAM]);
+
+ const { getByText, getAllByTitle, getByLabelText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ await waitFor(
+ () =>
+ expect(
+ getByText('user', {
+ exact: false,
+ }),
+ ).toBeInTheDocument(),
+ { timeout: 2000 },
+ );
+ expect(getByText('test-incident')).toBeInTheDocument();
+ expect(getByLabelText('Status warning')).toBeInTheDocument();
+ expect(() => getAllByTitle('Acknowledge')).toThrow(
+ 'Unable to find an element with the title: Acknowledge.',
+ );
+ expect(() => getAllByTitle('Resolve')).toThrow(
+ 'Unable to find an element with the title: Resolve.',
+ );
+
+ // assert links, mailto and hrefs, date calculation
+ expect(getAllByTitle('View in Splunk On-Call').length).toEqual(1);
+ });
+
it('Handle errors', async () => {
mockSplunkOnCallApi.getIncidents.mockRejectedValueOnce(
new Error('Error occurred'),
@@ -99,7 +133,7 @@ describe('Incidents', () => {
const { getByText, queryByTestId } = render(
wrapInTestApp(
-
+
,
),
);
diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx
index 3c557773fd..e2ec9609bd 100644
--- a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx
+++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx
@@ -49,9 +49,10 @@ const useStyles = makeStyles((theme: Theme) =>
type Props = {
refreshIncidents: boolean;
team: string;
+ readOnly: boolean;
};
-export const Incidents = ({ refreshIncidents, team }: Props) => {
+export const Incidents = ({ readOnly, refreshIncidents, team }: Props) => {
const classes = useStyles();
const api = useApi(splunkOnCallApiRef);
@@ -108,6 +109,7 @@ export const Incidents = ({ refreshIncidents, team }: Props) => {
key={index}
team={team}
incident={incident}
+ readOnly={readOnly}
/>
))
)}