Merge pull request #4681 from joaomilho/allow-trigger-button-without-pagerduty-key
Improve PagerDuty plugins
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
---
|
||||
'@backstage/core': minor
|
||||
'@backstage/plugin-pagerduty': patch
|
||||
---
|
||||
|
||||
- Adds onClick and other props to IconLinkVertical;
|
||||
- Allows TriggerButton component to render when pager duty key is missing;
|
||||
- Refactors TriggerButton and PagerDutyCard not to have shared state;
|
||||
- Removes the `action` prop of the IconLinkVertical component while adding `onClick`.
|
||||
|
||||
Instead of having an action including a button with onClick, now the whole component can be clickable making it easier to implement and having a better UX.
|
||||
|
||||
Before:
|
||||
|
||||
```ts
|
||||
const myLink: IconLinkVerticalProps = {
|
||||
label: 'Click me',
|
||||
action: <Button onClick={myAction} />,
|
||||
icon: <MyIcon onClick={myAction} />,
|
||||
};
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```ts
|
||||
const myLink: IconLinkVerticalProps = {
|
||||
label: 'Click me',
|
||||
onClick: myAction,
|
||||
icon: <MyIcon />,
|
||||
};
|
||||
```
|
||||
@@ -20,11 +20,13 @@ import LinkIcon from '@material-ui/icons/Link';
|
||||
import { Link as RouterLink } from '../Link';
|
||||
|
||||
export type IconLinkVerticalProps = {
|
||||
icon?: React.ReactNode;
|
||||
href?: string;
|
||||
color?: 'primary' | 'secondary';
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
action?: React.ReactNode;
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const useIconStyles = makeStyles(theme => ({
|
||||
@@ -33,56 +35,61 @@ const useIconStyles = makeStyles(theme => ({
|
||||
justifyItems: 'center',
|
||||
gridGap: 4,
|
||||
textAlign: 'center',
|
||||
'&:active': {
|
||||
cursor: 'grabbing',
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
color: 'gray',
|
||||
},
|
||||
primary: {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
secondary: {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
label: {
|
||||
fontSize: '0.7rem',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1.2,
|
||||
},
|
||||
linkStyle: {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
export function IconLinkVertical({
|
||||
icon = <LinkIcon />,
|
||||
href = '#',
|
||||
color = 'primary',
|
||||
disabled = false,
|
||||
action,
|
||||
...props
|
||||
href = '#',
|
||||
icon = <LinkIcon />,
|
||||
label,
|
||||
onClick,
|
||||
title,
|
||||
}: IconLinkVerticalProps) {
|
||||
const classes = useIconStyles();
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<Link
|
||||
title={title}
|
||||
className={classnames(classes.link, classes.disabled)}
|
||||
underline="none"
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
<span className={classes.label}>{props.label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if (action) {
|
||||
return (
|
||||
<Link className={classnames(classes.link, classes.linkStyle)} {...props}>
|
||||
{icon}
|
||||
{action}
|
||||
<span className={classes.label}>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className={classes.link} to={href} component={RouterLink} {...props}>
|
||||
<Link
|
||||
title={title}
|
||||
className={classnames(classes.link, classes[color])}
|
||||
to={href}
|
||||
component={RouterLink}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
<span className={classes.label}>{props.label}</span>
|
||||
<span className={classes.label}>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,3 +15,5 @@
|
||||
*/
|
||||
|
||||
export { HeaderIconLinkRow } from './HeaderIconLinkRow';
|
||||
|
||||
export type { IconLinkVerticalProps } from './IconLinkVertical';
|
||||
|
||||
@@ -19,12 +19,12 @@ import {
|
||||
SOURCE_LOCATION_ANNOTATION,
|
||||
EDIT_URL_ANNOTATION,
|
||||
} from '@backstage/catalog-model';
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, act, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { AboutCard } from './AboutCard';
|
||||
|
||||
describe('<AboutCard /> GitHub', () => {
|
||||
it('renders info and "view source" link', () => {
|
||||
it('renders info and "view source" link', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
@@ -41,7 +41,7 @@ describe('<AboutCard /> GitHub', () => {
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const { getByText } = render(
|
||||
const { getByText, getByTitle } = render(
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>,
|
||||
@@ -51,15 +51,21 @@ describe('<AboutCard /> GitHub', () => {
|
||||
'href',
|
||||
'https://github.com/backstage/backstage/blob/master/software.yaml',
|
||||
);
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'edithref',
|
||||
'https://github.com/backstage/backstage/edit/master/software.yaml',
|
||||
|
||||
const editButton = getByTitle('Edit Metadata');
|
||||
window.open = jest.fn();
|
||||
await act(async () => {
|
||||
fireEvent.click(editButton);
|
||||
});
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
`https://github.com/backstage/backstage/edit/master/software.yaml`,
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<AboutCard /> GitLab', () => {
|
||||
it('renders info and "view source" link', () => {
|
||||
it('renders info and "view source" link', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
@@ -76,25 +82,32 @@ describe('<AboutCard /> GitLab', () => {
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const { getByText } = render(
|
||||
const { getByText, getByTitle } = render(
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>,
|
||||
);
|
||||
|
||||
expect(getByText('service')).toBeInTheDocument();
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'https://gitlab.com/backstage/backstage/-/blob/master/software.yaml',
|
||||
);
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'edithref',
|
||||
'https://gitlab.com/backstage/backstage/-/edit/master/software.yaml',
|
||||
|
||||
const editButton = getByTitle('Edit Metadata');
|
||||
window.open = jest.fn();
|
||||
await act(async () => {
|
||||
fireEvent.click(editButton);
|
||||
});
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
`https://gitlab.com/backstage/backstage/-/edit/master/software.yaml`,
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<AboutCard /> BitBucket', () => {
|
||||
it('renders info and "view source" link', () => {
|
||||
it('renders info and "view source" link', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
@@ -111,7 +124,7 @@ describe('<AboutCard /> BitBucket', () => {
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const { getByText } = render(
|
||||
const { getByText, getByTitle } = render(
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>,
|
||||
@@ -121,15 +134,21 @@ describe('<AboutCard /> BitBucket', () => {
|
||||
'href',
|
||||
'https://bitbucket.org/backstage/backstage/src/master/software.yaml',
|
||||
);
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'edithref',
|
||||
'https://bitbucket.org/backstage/backstage/src/master/software.yaml?mode=edit&spa=0&at=master',
|
||||
|
||||
const editButton = getByTitle('Edit Metadata');
|
||||
window.open = jest.fn();
|
||||
await act(async () => {
|
||||
fireEvent.click(editButton);
|
||||
});
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
`https://bitbucket.org/backstage/backstage/src/master/software.yaml?mode=edit&spa=0&at=master`,
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<AboutCard /> custom links', () => {
|
||||
it('renders info and "view source" link', () => {
|
||||
it('renders info and "view source" link', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
@@ -149,7 +168,7 @@ describe('<AboutCard /> custom links', () => {
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const { getByText } = render(
|
||||
const { getByText, getByTitle } = render(
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>,
|
||||
@@ -159,9 +178,12 @@ describe('<AboutCard /> custom links', () => {
|
||||
'href',
|
||||
'https://another.place/backstage.git',
|
||||
);
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'edithref',
|
||||
'https://another.place',
|
||||
);
|
||||
|
||||
const editButton = getByTitle('Edit Metadata');
|
||||
window.open = jest.fn();
|
||||
await act(async () => {
|
||||
fireEvent.click(editButton);
|
||||
});
|
||||
expect(window.open).toHaveBeenCalledWith(`https://another.place`, '_blank');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
SOURCE_LOCATION_ANNOTATION,
|
||||
RELATION_PROVIDES_API,
|
||||
} from '@backstage/catalog-model';
|
||||
import { HeaderIconLinkRow } from '@backstage/core';
|
||||
import { HeaderIconLinkRow, IconLinkVerticalProps } from '@backstage/core';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
Card,
|
||||
@@ -105,11 +105,12 @@ export function AboutCard({ variant }: AboutCardProps) {
|
||||
const codeLink = getCodeLinkInfo(entity);
|
||||
// TODO: Also support RELATION_CONSUMES_API here
|
||||
const hasApis = entity.relations?.some(r => r.type === RELATION_PROVIDES_API);
|
||||
const viewInSource = {
|
||||
const viewInSource: IconLinkVerticalProps = {
|
||||
label: 'View Source',
|
||||
...codeLink,
|
||||
href: codeLink.href,
|
||||
icon: codeLink.icon,
|
||||
};
|
||||
const viewInTechDocs = {
|
||||
const viewInTechDocs: IconLinkVerticalProps = {
|
||||
label: 'View TechDocs',
|
||||
disabled: !entity.metadata.annotations?.['backstage.io/techdocs-ref'],
|
||||
icon: <DocsIcon />,
|
||||
@@ -117,7 +118,7 @@ export function AboutCard({ variant }: AboutCardProps) {
|
||||
entity.kind
|
||||
}/${entity.metadata.name}`,
|
||||
};
|
||||
const viewApi = {
|
||||
const viewApi: IconLinkVerticalProps = {
|
||||
title: hasApis ? '' : 'No APIs available',
|
||||
label: 'View API',
|
||||
disabled: !hasApis,
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
{
|
||||
isPagerDutyAvailable(entity) && (
|
||||
<Grid item md={6}>
|
||||
<PagerDutyCard entity={entity} />
|
||||
<EntityPagerDutyCard />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
+7
-7
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, waitFor, fireEvent, act } from '@testing-library/react';
|
||||
import { PagerDutyCard } from './PagerDutyCard';
|
||||
import { PagerDutyCard } from '../PagerDutyCard';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
ApiRegistry,
|
||||
createApiRef,
|
||||
} from '@backstage/core';
|
||||
import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../api';
|
||||
import { Service } from './types';
|
||||
import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api';
|
||||
import { Service } from '../types';
|
||||
|
||||
const mockPagerDutyApi: Partial<PagerDutyClient> = {
|
||||
getServiceByIntegrationKey: async () => [],
|
||||
@@ -138,7 +138,7 @@ describe('PageDutyCard', () => {
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => [service]);
|
||||
|
||||
const { getByText, queryByTestId, getByTestId, getByRole } = render(
|
||||
const { getByText, queryByTestId, getByRole } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -149,10 +149,10 @@ describe('PageDutyCard', () => {
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Service Directory')).toBeInTheDocument();
|
||||
expect(getByText('Create Incident')).toBeInTheDocument();
|
||||
const triggerButton = getByTestId('trigger-button');
|
||||
|
||||
const triggerLink = getByText('Create Incident');
|
||||
await act(async () => {
|
||||
fireEvent.click(triggerButton);
|
||||
fireEvent.click(triggerLink);
|
||||
});
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
+49
-33
@@ -14,43 +14,50 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useApi, Progress, HeaderIconLinkRow } from '@backstage/core';
|
||||
import {
|
||||
useApi,
|
||||
Progress,
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
} from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { Card, CardHeader, Divider, CardContent } from '@material-ui/core';
|
||||
import { Incidents } from './Incident';
|
||||
import { EscalationPolicy } from './Escalation';
|
||||
import { Incidents } from '../Incident';
|
||||
import { EscalationPolicy } from '../Escalation';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { pagerDutyApiRef, UnauthorizedError } from '../api';
|
||||
import { pagerDutyApiRef, UnauthorizedError } from '../../api';
|
||||
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
|
||||
import { MissingTokenError } from './Errors/MissingTokenError';
|
||||
import { MissingTokenError } from '../Errors/MissingTokenError';
|
||||
import WebIcon from '@material-ui/icons/Web';
|
||||
import { PAGERDUTY_INTEGRATION_KEY } from './constants';
|
||||
import { TriggerButton, useShowDialog } from './TriggerButton';
|
||||
import { usePagerdutyEntity } from '../../hooks';
|
||||
import { PAGERDUTY_INTEGRATION_KEY } from '../constants';
|
||||
import { TriggerDialog } from '../TriggerDialog';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]);
|
||||
|
||||
export const PagerDutyCard = () => {
|
||||
const { entity } = useEntity();
|
||||
const { integrationKey } = usePagerdutyEntity();
|
||||
const api = useApi(pagerDutyApiRef);
|
||||
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
|
||||
const integrationKey = entity.metadata.annotations![
|
||||
PAGERDUTY_INTEGRATION_KEY
|
||||
];
|
||||
const setShowDialog = useShowDialog()[1];
|
||||
const [dialogShown, setDialogShown] = useState<boolean>(false);
|
||||
|
||||
const showDialog = useCallback(() => {
|
||||
setShowDialog(true);
|
||||
}, [setShowDialog]);
|
||||
setDialogShown(true);
|
||||
}, [setDialogShown]);
|
||||
const hideDialog = useCallback(() => {
|
||||
setDialogShown(false);
|
||||
}, [setDialogShown]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setRefreshIncidents(x => !x);
|
||||
}, []);
|
||||
|
||||
const { value: service, loading, error } = useAsync(async () => {
|
||||
const services = await api.getServiceByIntegrationKey(integrationKey);
|
||||
const services = await api.getServiceByIntegrationKey(
|
||||
integrationKey as string,
|
||||
);
|
||||
|
||||
return {
|
||||
id: services[0].id,
|
||||
@@ -76,32 +83,41 @@ export const PagerDutyCard = () => {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
const serviceLink = {
|
||||
const serviceLink: IconLinkVerticalProps = {
|
||||
label: 'Service Directory',
|
||||
href: service!.url,
|
||||
icon: <WebIcon />,
|
||||
};
|
||||
|
||||
const triggerLink = {
|
||||
const triggerLink: IconLinkVerticalProps = {
|
||||
label: 'Create Incident',
|
||||
action: <TriggerButton design="link" onIncidentCreated={handleRefresh} />,
|
||||
icon: <AlarmAddIcon onClick={showDialog} />,
|
||||
onClick: showDialog,
|
||||
icon: <AlarmAddIcon />,
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="PagerDuty"
|
||||
subheader={<HeaderIconLinkRow links={[serviceLink, triggerLink]} />}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Incidents
|
||||
serviceId={service!.id}
|
||||
refreshIncidents={refreshIncidents}
|
||||
<>
|
||||
<Card data-testid="pagerduty-card">
|
||||
<CardHeader
|
||||
title="PagerDuty"
|
||||
subheader={<HeaderIconLinkRow links={[serviceLink, triggerLink]} />}
|
||||
/>
|
||||
<EscalationPolicy policyId={service!.policyId} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Incidents
|
||||
serviceId={service!.id}
|
||||
refreshIncidents={refreshIncidents}
|
||||
/>
|
||||
<EscalationPolicy policyId={service!.policyId} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TriggerDialog
|
||||
data-testid="trigger-dialog"
|
||||
showDialog={dialogShown}
|
||||
handleDialog={hideDialog}
|
||||
onIncidentCreated={handleRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import {
|
||||
ApiRegistry,
|
||||
alertApiRef,
|
||||
createApiRef,
|
||||
ApiProvider,
|
||||
IdentityApi,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
import { pagerDutyApiRef } from '../../api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { TriggerButton } from './';
|
||||
|
||||
describe('TriggerButton', () => {
|
||||
const mockIdentityApi: Partial<IdentityApi> = {
|
||||
getUserId: () => 'guest@example.com',
|
||||
};
|
||||
|
||||
const mockTriggerAlarmFn = jest.fn();
|
||||
const mockPagerDutyApi = {
|
||||
triggerAlarm: mockTriggerAlarmFn,
|
||||
};
|
||||
|
||||
const apis = ApiRegistry.from([
|
||||
[
|
||||
alertApiRef,
|
||||
createApiRef({
|
||||
id: 'core.alert',
|
||||
description: 'Used to report alerts and forward them to the app',
|
||||
}),
|
||||
],
|
||||
[identityApiRef, mockIdentityApi],
|
||||
[pagerDutyApiRef, mockPagerDutyApi],
|
||||
]);
|
||||
|
||||
it('renders the trigger button, opens and closes dialog', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<TriggerButton />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
const triggerButton = screen.getByText('Create Incident');
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(triggerButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const closeButton = screen.getByText('Close');
|
||||
await act(async () => {
|
||||
fireEvent.click(closeButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the trigger button with children', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<TriggerButton>Send an alert</TriggerButton>
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Send an alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a disabled trigger button if entity does not include key', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
},
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<TriggerButton />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
const triggerButton = screen.getByText('Missing integration key');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(triggerButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,19 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useCallback, PropsWithChildren } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import React, { useCallback, PropsWithChildren, useState } from 'react';
|
||||
import { makeStyles, Button } from '@material-ui/core';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
import { usePagerdutyEntity } from '../../hooks';
|
||||
import { TriggerDialog } from '../TriggerDialog';
|
||||
import { PAGERDUTY_INTEGRATION_KEY } from '../constants';
|
||||
|
||||
export interface TriggerButtonProps {
|
||||
design: 'link' | 'button';
|
||||
onIncidentCreated?: () => void;
|
||||
}
|
||||
export type TriggerButtonProps = {};
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
buttonStyle: {
|
||||
@@ -35,31 +30,14 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
},
|
||||
},
|
||||
triggerAlarm: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
fontSize: '0.7rem',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1.2,
|
||||
lineHeight: 1.5,
|
||||
'&:hover, &:focus, &.focus': {
|
||||
backgroundColor: 'transparent',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const useShowDialog = createGlobalState(false);
|
||||
|
||||
export function TriggerButton({
|
||||
design,
|
||||
onIncidentCreated,
|
||||
children,
|
||||
}: PropsWithChildren<TriggerButtonProps>) {
|
||||
const { buttonStyle, triggerAlarm } = useStyles();
|
||||
const { entity } = useEntity();
|
||||
const [dialogShown = false, setDialogShown] = useShowDialog();
|
||||
const { buttonStyle } = useStyles();
|
||||
const { integrationKey } = usePagerdutyEntity();
|
||||
const [dialogShown, setDialogShown] = useState<boolean>(false);
|
||||
|
||||
const showDialog = useCallback(() => {
|
||||
setDialogShown(true);
|
||||
@@ -68,27 +46,22 @@ export function TriggerButton({
|
||||
setDialogShown(false);
|
||||
}, [setDialogShown]);
|
||||
|
||||
const integrationKey = entity.metadata.annotations![
|
||||
PAGERDUTY_INTEGRATION_KEY
|
||||
];
|
||||
|
||||
const disabled = !integrationKey;
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
data-testid="trigger-button"
|
||||
{...(design === 'link' && { color: 'secondary' })}
|
||||
onClick={showDialog}
|
||||
className={design === 'link' ? triggerAlarm : buttonStyle}
|
||||
variant="contained"
|
||||
className={disabled ? '' : buttonStyle}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children ?? 'Create Incident'}
|
||||
{integrationKey
|
||||
? children ?? 'Create Incident'
|
||||
: 'Missing integration key'}
|
||||
</Button>
|
||||
<TriggerDialog
|
||||
showDialog={dialogShown}
|
||||
handleDialog={hideDialog}
|
||||
name={entity.metadata.name}
|
||||
integrationKey={integrationKey}
|
||||
onIncidentCreated={onIncidentCreated}
|
||||
/>
|
||||
{integrationKey && (
|
||||
<TriggerDialog showDialog={dialogShown} handleDialog={hideDialog} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, fireEvent, act } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { fireEvent, act } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import {
|
||||
ApiRegistry,
|
||||
alertApiRef,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from '@backstage/core';
|
||||
import { pagerDutyApiRef } from '../../api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { TriggerDialog } from './TriggerDialog';
|
||||
|
||||
describe('TriggerDialog', () => {
|
||||
@@ -62,18 +63,16 @@ describe('TriggerDialog', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const { getByText, getByRole, getByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
const { getByText, getByRole, getByTestId } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<TriggerDialog
|
||||
showDialog
|
||||
handleDialog={() => {}}
|
||||
name={entity.metadata.name}
|
||||
integrationKey="abc123"
|
||||
onIncidentCreated={() => {}}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
@@ -29,22 +29,20 @@ import { useApi, alertApiRef, identityApiRef } from '@backstage/core';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import { pagerDutyApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { usePagerdutyEntity } from '../../hooks';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
integrationKey: string;
|
||||
showDialog: boolean;
|
||||
handleDialog: () => void;
|
||||
onIncidentCreated?: () => void;
|
||||
};
|
||||
|
||||
export const TriggerDialog = ({
|
||||
name,
|
||||
integrationKey,
|
||||
showDialog,
|
||||
handleDialog,
|
||||
onIncidentCreated: onIncidentCreated,
|
||||
}: Props) => {
|
||||
const { name, integrationKey } = usePagerdutyEntity();
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const userName = identityApi.getUserId();
|
||||
@@ -54,7 +52,7 @@ export const TriggerDialog = ({
|
||||
const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn(
|
||||
async (descriptions: string) =>
|
||||
await api.triggerAlarm({
|
||||
integrationKey,
|
||||
integrationKey: integrationKey as string,
|
||||
source: window.location.toString(),
|
||||
description: descriptions,
|
||||
userName,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { PAGERDUTY_INTEGRATION_KEY } from '../components/constants';
|
||||
|
||||
export function usePagerdutyEntity() {
|
||||
const { entity } = useEntity();
|
||||
const integrationKey =
|
||||
entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY];
|
||||
const name = entity.metadata.name;
|
||||
|
||||
return { integrationKey, name };
|
||||
}
|
||||
@@ -132,12 +132,13 @@ describe('SplunkOnCallCard', () => {
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the dialog when trigger button is clicked', async () => {
|
||||
mockSplunkOnCallApi.getUsers = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => [MOCKED_USER]);
|
||||
|
||||
const { getByText, queryByTestId, getByTestId, getByRole } = render(
|
||||
const { getByText, queryByTestId, getByRole } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<SplunkOnCallCard entity={entity} />
|
||||
@@ -146,7 +147,7 @@ describe('SplunkOnCallCard', () => {
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Create Incident')).toBeInTheDocument();
|
||||
const triggerButton = getByTestId('trigger-button');
|
||||
const triggerButton = getByText('Create Incident');
|
||||
await act(async () => {
|
||||
fireEvent.click(triggerButton);
|
||||
});
|
||||
|
||||
@@ -21,11 +21,10 @@ import {
|
||||
MissingAnnotationEmptyState,
|
||||
configApiRef,
|
||||
EmptyState,
|
||||
IconLinkVerticalProps,
|
||||
} from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Button,
|
||||
makeStyles,
|
||||
Card,
|
||||
CardHeader,
|
||||
Divider,
|
||||
@@ -42,22 +41,6 @@ import { TriggerDialog } from './TriggerDialog';
|
||||
import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError';
|
||||
import { User } from './types';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
triggerAlarm: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
fontSize: '0.7rem',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1.2,
|
||||
lineHeight: 1.5,
|
||||
'&:hover, &:focus, &.focus': {
|
||||
backgroundColor: 'transparent',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const SPLUNK_ON_CALL_TEAM = 'splunk.com/on-call-team';
|
||||
|
||||
export const MissingTeamAnnotation = () => (
|
||||
@@ -82,7 +65,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SplunkOnCallCard = ({ entity }: Props) => {
|
||||
const classes = useStyles();
|
||||
const config = useApi(configApiRef);
|
||||
const api = useApi(splunkOnCallApiRef);
|
||||
const [showDialog, setShowDialog] = useState<boolean>(false);
|
||||
@@ -159,19 +141,11 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const triggerLink = {
|
||||
const triggerLink: IconLinkVerticalProps = {
|
||||
label: 'Create Incident',
|
||||
action: (
|
||||
<Button
|
||||
data-testid="trigger-button"
|
||||
color="secondary"
|
||||
onClick={handleDialog}
|
||||
className={classes.triggerAlarm}
|
||||
>
|
||||
Create Incident
|
||||
</Button>
|
||||
),
|
||||
icon: <AlarmAddIcon onClick={handleDialog} />,
|
||||
onClick: handleDialog,
|
||||
color: 'secondary',
|
||||
icon: <AlarmAddIcon />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user