Merge pull request #9635 from mdb/mdb/splunk-on-call-read-only
[Plugin] expose 'read only' configuration option in Splunk On-Call plugin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-splunk-on-call': patch
|
||||
---
|
||||
|
||||
The Splunk On-Call plugin now supports an optional `readOnly` property (`<SplunkOnCallEntityCard readOnly />`) for suppressing the rendering of incident trigger-acknowledge-resolve controls from the Backstage UI.
|
||||
@@ -45,6 +45,14 @@ const overviewContent = (
|
||||
</EntitySwitch>
|
||||
```
|
||||
|
||||
### `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
|
||||
<EntitySplunkOnCallCard readOnly />
|
||||
```
|
||||
|
||||
## 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 (`<EntitySplunkOnCallCard readOnly />`) 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.
|
||||
|
||||
@@ -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)
|
||||
//
|
||||
|
||||
@@ -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(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={mockEntityNoIncidents}>
|
||||
<EntitySplunkOnCallCard readOnly />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
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()
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Incidents team={teamName} refreshIncidents={refreshIncidents} />
|
||||
<Incidents
|
||||
readOnly={readOnly || false}
|
||||
team={teamName}
|
||||
refreshIncidents={refreshIncidents}
|
||||
/>
|
||||
{usersHashMap && team && (
|
||||
<EscalationPolicy team={teamName} users={usersHashMap} />
|
||||
)}
|
||||
@@ -259,7 +270,7 @@ export const EntitySplunkOnCallCard = () => {
|
||||
</Typography>,
|
||||
<HeaderIconLinkRow
|
||||
key="incident_trigger"
|
||||
links={[serviceLink, triggerLink]}
|
||||
links={!readOnly ? [serviceLink, triggerLink] : [serviceLink]}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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 && (
|
||||
<ListItemSecondaryAction>
|
||||
<IncidentAction
|
||||
currentPhase={incident.currentPhase || ''}
|
||||
incidentId={incident.entityId}
|
||||
resolveAction={handleResolveIncident}
|
||||
acknowledgeAction={handleAcknowledgeIncident}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<IncidentAction
|
||||
currentPhase={incident.currentPhase || ''}
|
||||
incidentId={incident.entityId}
|
||||
resolveAction={handleResolveIncident}
|
||||
acknowledgeAction={handleAcknowledgeIncident}
|
||||
/>
|
||||
)}
|
||||
<Tooltip title="View in Splunk On-Call" placement="top">
|
||||
<IconButton
|
||||
href={incident.incidentLink}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('Incidents', () => {
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<Incidents refreshIncidents={false} team="test" />
|
||||
<Incidents readOnly={false} refreshIncidents={false} team="test" />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -68,7 +68,7 @@ describe('Incidents', () => {
|
||||
} = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<Incidents team="test" refreshIncidents={false} />
|
||||
<Incidents readOnly={false} team="test" refreshIncidents={false} />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -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(
|
||||
<ApiProvider apis={apis}>
|
||||
<Incidents readOnly team="test" refreshIncidents={false} />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<Incidents team="test" refreshIncidents={false} />
|
||||
<Incidents readOnly={false} team="test" refreshIncidents={false} />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user