Merge pull request #9362 from mdb/mdb/new-splunk-on-call-annotation
[Plugin] Splunk On-Call plugin support for 'splunk.com/on-call-routing-key' annotation
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-splunk-on-call': patch
|
||||
---
|
||||
|
||||
Add Splunk On-Call plugin support for a `splunk.com/on-call-routing-key` annotation. If the `splunk.com/on-call-routing-key` is provided, the plugin displays a Splunk On-Call card for each of the teams associated with the routing key.
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This plugin displays Splunk On-Call, formerly VictorOps, information about an entity.
|
||||
This plugin displays Splunk On-Call (formerly VictorOps) information associated with an entity.
|
||||
|
||||
There is a way to trigger an new incident directly to specific users or/and specific teams.
|
||||
It also provides the ability to trigger new incidents to specific users and/or specific teams from within Backstage.
|
||||
|
||||
This plugin requires that entities are annotated with a team name. See more further down in this document.
|
||||
This plugin requires that entities feature either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation. See below for further details.
|
||||
|
||||
This plugin provides:
|
||||
|
||||
- A list of incidents
|
||||
- A way to trigger a new incident to specific users or/and teams
|
||||
- A way to trigger a new incident to specific users and/or teams
|
||||
- A way to acknowledge/resolve an incident
|
||||
- Information details about the persons on-call
|
||||
|
||||
@@ -47,7 +47,7 @@ const overviewContent = (
|
||||
|
||||
## Client configuration
|
||||
|
||||
In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide a REST Endpoint.
|
||||
In order to be able to perform certain actions (create-acknowledge-resolve an action), you need to provide a REST Endpoint.
|
||||
|
||||
To enable the REST Endpoint integration you can go on https://portal.victorops.com/ inside Integrations > 3rd Party Integrations > REST – Generic.
|
||||
You can now copy the URL to notify: `<SPLUNK_ON_CALL_REST_ENDPOINT>/$routing_key`
|
||||
@@ -76,12 +76,22 @@ In addition, to make certain API calls (trigger-resolve-acknowledge an incident)
|
||||
|
||||
### Adding your team name to the entity annotation
|
||||
|
||||
The information displayed for each entity is based on the team name.
|
||||
If you want to use this plugin for an entity, you need to label it with the below annotation:
|
||||
The information displayed for each entity is based on either an associated team name or an associated routing key.
|
||||
|
||||
To use this plugin for an entity, the entity must be labeled with either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation.
|
||||
|
||||
For example, by specifying a `splunk.com/on-call-team`, the plugin displays Splunk On-Call data associated with the specified team:
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
splunk.com/on-call-team': <SPLUNK_ON_CALL_TEAM_NAME>
|
||||
splunk.com/on-call-team: <SPLUNK_ON_CALL_TEAM_NAME>
|
||||
```
|
||||
|
||||
Alternatively, by specifying a `splunk.com/on-call-routing-key`, the plugin displays Splunk On-Call data associated with _each_ of the teams associated with the specified routing key:
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
splunk.com/on-call-routing-key: <SPLUNK_ON_CALL_ROUTING_KEY>
|
||||
```
|
||||
|
||||
### Create the Routing Key
|
||||
|
||||
@@ -51,6 +51,10 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
|
||||
//
|
||||
// (undocumented)
|
||||
getOnCallUsers(): Promise<OnCall[]>;
|
||||
// Warning: (ae-forgotten-export) The symbol "RoutingKey" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
getRoutingKeys(): Promise<RoutingKey[]>;
|
||||
// Warning: (ae-forgotten-export) The symbol "Team" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
OnCall,
|
||||
User,
|
||||
EscalationPolicyInfo,
|
||||
RoutingKey,
|
||||
Team,
|
||||
} from '../components/types';
|
||||
import {
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
RequestOptions,
|
||||
ListUserResponse,
|
||||
EscalationPolicyResponse,
|
||||
ListRoutingKeyResponse,
|
||||
} from './types';
|
||||
import {
|
||||
createApiRef,
|
||||
@@ -82,6 +84,15 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
|
||||
return teams;
|
||||
}
|
||||
|
||||
async getRoutingKeys(): Promise<RoutingKey[]> {
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
'proxy',
|
||||
)}/splunk-on-call/v1/org/routing-keys`;
|
||||
const { routingKeys } = await this.getByUrl<ListRoutingKeyResponse>(url);
|
||||
|
||||
return routingKeys;
|
||||
}
|
||||
|
||||
async getUsers(): Promise<User[]> {
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
'proxy',
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import {
|
||||
EscalationPolicyInfo,
|
||||
Incident,
|
||||
RoutingKey,
|
||||
Team,
|
||||
User,
|
||||
} from '../components/types';
|
||||
@@ -88,6 +89,18 @@ export const MOCK_TEAM: Team = {
|
||||
isDefaultTeam: false,
|
||||
};
|
||||
|
||||
export const MOCK_ROUTING_KEY: RoutingKey = {
|
||||
routingKey: 'test-routing-key',
|
||||
targets: [
|
||||
{
|
||||
policyName: 'some policy',
|
||||
policySlug: MOCK_TEAM.slug || '',
|
||||
_teamUrl: `/api-public/v1/team/${MOCK_TEAM.slug}`,
|
||||
},
|
||||
],
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
export const MOCK_TEAM_NO_INCIDENTS: Team = {
|
||||
...MOCK_TEAM,
|
||||
name: 'test-noincidents',
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
EscalationPolicyInfo,
|
||||
Incident,
|
||||
OnCall,
|
||||
RoutingKey,
|
||||
Team,
|
||||
User,
|
||||
} from '../components/types';
|
||||
@@ -65,6 +66,11 @@ export interface SplunkOnCallApi {
|
||||
*/
|
||||
getTeams(): Promise<Team[]>;
|
||||
|
||||
/**
|
||||
* Get a list of routing keys for your organization.
|
||||
*/
|
||||
getRoutingKeys(): Promise<RoutingKey[]>;
|
||||
|
||||
/**
|
||||
* Get a list of escalation policies for your organization.
|
||||
*/
|
||||
@@ -80,6 +86,11 @@ export type ListUserResponse = {
|
||||
_selfUrl?: string;
|
||||
};
|
||||
|
||||
export type ListRoutingKeyResponse = {
|
||||
routingKeys: RoutingKey[];
|
||||
_selfUrl?: string;
|
||||
};
|
||||
|
||||
export type IncidentsResponse = {
|
||||
incidents: Incident[];
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
MOCKED_ON_CALL,
|
||||
MOCKED_USER,
|
||||
MOCK_INCIDENT,
|
||||
MOCK_ROUTING_KEY,
|
||||
MOCK_TEAM,
|
||||
MOCK_TEAM_NO_INCIDENTS,
|
||||
} from '../api/mocks';
|
||||
@@ -45,6 +46,7 @@ const mockSplunkOnCallApi: Partial<SplunkOnCallClient> = {
|
||||
getIncidents: async () => [MOCK_INCIDENT],
|
||||
getOnCallUsers: async () => MOCKED_ON_CALL,
|
||||
getTeams: async () => [MOCK_TEAM],
|
||||
getRoutingKeys: async () => [MOCK_ROUTING_KEY],
|
||||
getEscalationPolicies: async () => ESCALATION_POLICIES,
|
||||
};
|
||||
|
||||
@@ -71,6 +73,26 @@ const mockEntity = {
|
||||
},
|
||||
} as Entity;
|
||||
|
||||
const mockEntityWithRoutingKeyAnnotation = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'splunkoncall-test',
|
||||
annotations: {
|
||||
'splunk.com/on-call-routing-key': MOCK_ROUTING_KEY.routingKey,
|
||||
},
|
||||
},
|
||||
} as Entity;
|
||||
|
||||
const mockEntityNoAnnotation = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'splunkoncall-test',
|
||||
annotations: {},
|
||||
},
|
||||
} as Entity;
|
||||
|
||||
const mockEntityNoIncidents = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
@@ -109,6 +131,34 @@ describe('SplunkOnCallCard', () => {
|
||||
expect(getByText('Empty escalation policy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles a "splunk.com/on-call-routing-key" annotation', async () => {
|
||||
mockSplunkOnCallApi.getUsers = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => [MOCKED_USER]);
|
||||
mockSplunkOnCallApi.getRoutingKeys = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => [MOCK_ROUTING_KEY]);
|
||||
mockSplunkOnCallApi.getTeams = jest
|
||||
.fn()
|
||||
.mockImplementation(async () => [MOCK_TEAM]);
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={mockEntityWithRoutingKeyAnnotation}>
|
||||
<EntitySplunkOnCallCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText(`Team: ${MOCK_TEAM.name}`)).toBeInTheDocument();
|
||||
await waitFor(
|
||||
() => expect(getByText('test-incident')).toBeInTheDocument(),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('Handles custom error for missing token', async () => {
|
||||
mockSplunkOnCallApi.getUsers = jest
|
||||
.fn()
|
||||
@@ -151,6 +201,20 @@ describe('SplunkOnCallCard', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles warning for missing required annotations', async () => {
|
||||
const { getAllByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={mockEntityNoAnnotation}>
|
||||
<EntitySplunkOnCallCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getAllByText('Missing Annotation').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('handles warning for incorrect team annotation', async () => {
|
||||
mockSplunkOnCallApi.getUsers = jest
|
||||
.fn()
|
||||
@@ -170,7 +234,37 @@ describe('SplunkOnCallCard', () => {
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(
|
||||
getByText('Could not find team named "test" in the Splunk On-Call API'),
|
||||
getByText(
|
||||
'Splunk On-Call API returned no record of teams associated with the "test" team name',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles warning for incorrect routing key annotation', async () => {
|
||||
mockSplunkOnCallApi.getUsers = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => [MOCKED_USER]);
|
||||
mockSplunkOnCallApi.getRoutingKeys = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => [MOCK_ROUTING_KEY]);
|
||||
mockSplunkOnCallApi.getTeams = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => []);
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={mockEntityWithRoutingKeyAnnotation}>
|
||||
<EntitySplunkOnCallCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(
|
||||
getByText(
|
||||
`Splunk On-Call API returned no record of teams associated with the "${MOCK_ROUTING_KEY.routingKey}" routing key`,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Divider,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
|
||||
@@ -32,8 +33,7 @@ import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError';
|
||||
import { EscalationPolicy } from './Escalation';
|
||||
import { Incidents } from './Incident';
|
||||
import { TriggerDialog } from './TriggerDialog';
|
||||
import { User } from './types';
|
||||
|
||||
import { Team, User } from './types';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import {
|
||||
@@ -45,20 +45,49 @@ import {
|
||||
} from '@backstage/core-components';
|
||||
|
||||
export const SPLUNK_ON_CALL_TEAM = 'splunk.com/on-call-team';
|
||||
export const SPLUNK_ON_CALL_ROUTING_KEY = 'splunk.com/on-call-routing-key';
|
||||
|
||||
export const MissingTeamAnnotation = () => (
|
||||
<MissingAnnotationEmptyState annotation={SPLUNK_ON_CALL_TEAM} />
|
||||
export const MissingAnnotation = () => (
|
||||
<div>
|
||||
<Typography>
|
||||
The Splunk On Call plugin requires setting either the{' '}
|
||||
<code>{SPLUNK_ON_CALL_TEAM}</code> or the{' '}
|
||||
<code>{SPLUNK_ON_CALL_ROUTING_KEY}</code> annotation.
|
||||
</Typography>
|
||||
<MissingAnnotationEmptyState annotation={SPLUNK_ON_CALL_TEAM} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const InvalidTeamAnnotation = ({ teamName }: { teamName: string }) => (
|
||||
<CardContent>
|
||||
<EmptyState
|
||||
title={`Could not find team named "${teamName}" in the Splunk On-Call API`}
|
||||
missing="info"
|
||||
description={`Escalation Policy and incident information unavailable. Please verify that the team you added "${teamName}" is valid if you want to enable Splunk On-Call.`}
|
||||
/>
|
||||
</CardContent>
|
||||
);
|
||||
export const InvalidAnnotation = ({
|
||||
teamName,
|
||||
routingKey,
|
||||
}: {
|
||||
teamName: string | undefined;
|
||||
routingKey: string | undefined;
|
||||
}) => {
|
||||
let titleSuffix = 'provided annotation';
|
||||
|
||||
if (routingKey) {
|
||||
titleSuffix = `"${routingKey}" routing key`;
|
||||
}
|
||||
|
||||
if (teamName) {
|
||||
titleSuffix = `"${teamName}" team name`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title="Splunk On-Call" />
|
||||
<CardContent>
|
||||
<EmptyState
|
||||
title={`Splunk On-Call API returned no record of teams associated with the ${titleSuffix}`}
|
||||
missing="info"
|
||||
description="Escalation Policy and incident information unavailable. Splunk On-Call requires a valid team name or routing key."
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const MissingEventsRestEndpoint = () => (
|
||||
<CardContent>
|
||||
@@ -71,15 +100,28 @@ export const MissingEventsRestEndpoint = () => (
|
||||
);
|
||||
|
||||
export const isSplunkOnCallAvailable = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]);
|
||||
Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]) ||
|
||||
Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_ROUTING_KEY]);
|
||||
|
||||
const useStyles = makeStyles({
|
||||
onCallCard: {
|
||||
marginBottom: '1em',
|
||||
},
|
||||
});
|
||||
|
||||
export const EntitySplunkOnCallCard = () => {
|
||||
const classes = useStyles();
|
||||
const config = useApi(configApiRef);
|
||||
const api = useApi(splunkOnCallApiRef);
|
||||
const { entity } = useEntity();
|
||||
const [showDialog, setShowDialog] = useState<boolean>(false);
|
||||
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
|
||||
const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM];
|
||||
const teamAnnotation = entity
|
||||
? entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]
|
||||
: undefined;
|
||||
const routingKeyAnnotation = entity
|
||||
? entity.metadata.annotations![SPLUNK_ON_CALL_ROUTING_KEY]
|
||||
: undefined;
|
||||
|
||||
const eventsRestEndpoint =
|
||||
config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null;
|
||||
@@ -93,7 +135,7 @@ export const EntitySplunkOnCallCard = () => {
|
||||
}, []);
|
||||
|
||||
const {
|
||||
value: usersAndTeam,
|
||||
value: usersAndTeams,
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
@@ -108,10 +150,38 @@ export const EntitySplunkOnCallCard = () => {
|
||||
{},
|
||||
);
|
||||
const teams = await api.getTeams();
|
||||
const foundTeam = teams.find(teamValue => teamValue.name === team);
|
||||
return { usersHashMap, foundTeam };
|
||||
let foundTeams = [
|
||||
teams.find(teamValue => teamValue.name === teamAnnotation),
|
||||
].filter(team => team !== undefined);
|
||||
|
||||
if (!foundTeams.length && routingKeyAnnotation) {
|
||||
const routingKeys = await api.getRoutingKeys();
|
||||
const foundRoutingKey = routingKeys.find(
|
||||
key => key.routingKey === routingKeyAnnotation,
|
||||
);
|
||||
foundTeams = foundRoutingKey
|
||||
? foundRoutingKey.targets
|
||||
.map(target => {
|
||||
const teamUrlParts = target._teamUrl.split('/');
|
||||
const teamSlug = teamUrlParts[teamUrlParts.length - 1];
|
||||
|
||||
return teams.find(teamValue => teamValue.slug === teamSlug);
|
||||
})
|
||||
.filter(team => team !== undefined)
|
||||
: [];
|
||||
}
|
||||
|
||||
return { usersHashMap, foundTeams };
|
||||
});
|
||||
|
||||
if (!teamAnnotation && !routingKeyAnnotation) {
|
||||
return <MissingAnnotation />;
|
||||
}
|
||||
|
||||
if (!eventsRestEndpoint) {
|
||||
return <MissingEventsRestEndpoint />;
|
||||
}
|
||||
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return <MissingApiKeyOrApiIdError />;
|
||||
}
|
||||
@@ -128,27 +198,32 @@ export const EntitySplunkOnCallCard = () => {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
const Content = () => {
|
||||
if (!team) {
|
||||
return <MissingTeamAnnotation />;
|
||||
}
|
||||
if (!usersAndTeams?.foundTeams || !usersAndTeams?.foundTeams.length) {
|
||||
return (
|
||||
<InvalidAnnotation
|
||||
teamName={teamAnnotation}
|
||||
routingKey={routingKeyAnnotation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!usersAndTeam?.foundTeam) {
|
||||
return <InvalidTeamAnnotation teamName={team} />;
|
||||
}
|
||||
|
||||
if (!eventsRestEndpoint) {
|
||||
return <MissingEventsRestEndpoint />;
|
||||
}
|
||||
const Content = ({
|
||||
team,
|
||||
usersHashMap,
|
||||
}: {
|
||||
team: Team | undefined;
|
||||
usersHashMap: any;
|
||||
}) => {
|
||||
const teamName = team?.name ?? '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Incidents team={team} refreshIncidents={refreshIncidents} />
|
||||
{usersAndTeam?.usersHashMap && team && (
|
||||
<EscalationPolicy team={team} users={usersAndTeam.usersHashMap} />
|
||||
<Incidents team={teamName} refreshIncidents={refreshIncidents} />
|
||||
{usersHashMap && team && (
|
||||
<EscalationPolicy team={teamName} users={usersHashMap} />
|
||||
)}
|
||||
<TriggerDialog
|
||||
team={team}
|
||||
team={teamName}
|
||||
showDialog={showDialog}
|
||||
handleDialog={handleDialog}
|
||||
onIncidentCreated={handleRefresh}
|
||||
@@ -170,22 +245,30 @@ export const EntitySplunkOnCallCard = () => {
|
||||
icon: <WebIcon />,
|
||||
};
|
||||
|
||||
const teams = usersAndTeams?.foundTeams || [];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Splunk On-Call"
|
||||
subheader={[
|
||||
<Typography key="team_name">Team: {team}</Typography>,
|
||||
<HeaderIconLinkRow
|
||||
key="incident_trigger"
|
||||
links={[serviceLink, triggerLink]}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Content />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<>
|
||||
{teams.map((team, i) => (
|
||||
<Card key={i} className={classes.onCallCard}>
|
||||
<CardHeader
|
||||
title="Splunk On-Call"
|
||||
subheader={[
|
||||
<Typography key="team_name">
|
||||
Team: {team && team.name ? team.name : ''}
|
||||
</Typography>,
|
||||
<HeaderIconLinkRow
|
||||
key="incident_trigger"
|
||||
links={[serviceLink, triggerLink]}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Content team={team} usersHashMap={usersAndTeams?.usersHashMap} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -117,3 +117,15 @@ export type EscalationPolicyTeam = {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type RoutingKey = {
|
||||
routingKey: string;
|
||||
targets: RoutingKeyTarget[];
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
export type RoutingKeyTarget = {
|
||||
policyName: string;
|
||||
policySlug: string;
|
||||
_teamUrl: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user