Merge branch 'master' into fix/BCKSTG-162-broken-custom-styles-on-searc
Signed-off-by: Emma Indal <emma.indahl@gmail.com>
This commit is contained in:
@@ -30,7 +30,7 @@ describe('<ApiDefinitionCard />', () => {
|
||||
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
|
||||
getApiDefinitionWidget: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('<ApiTypeTitle />', () => {
|
||||
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
|
||||
getApiDefinitionWidget: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('<ConsumedApisCard />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('<HasApisCard />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('<ProvidedApisCard />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('<ConsumingComponentsCard />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('<ProvidingComponentsCard />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import {
|
||||
EntityPeekAheadPopover,
|
||||
EntityPeekAheadPopoverProps,
|
||||
@@ -80,7 +80,7 @@ const defaultArgs = {
|
||||
export default {
|
||||
title: 'Catalog /PeekAheadPopover',
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<>
|
||||
<TestApiProvider
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { EntityRefLink, EntityRefLinkProps } from './EntityRefLink';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { entityRouteRef } from '../../routes';
|
||||
@@ -26,7 +26,7 @@ const defaultArgs = {
|
||||
export default {
|
||||
title: 'Catalog /EntityRefLink',
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(<Story />, {
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { EntityRefLinks, EntityRefLinksProps } from './EntityRefLinks';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
@@ -27,7 +27,7 @@ const defaultArgs = {
|
||||
export default {
|
||||
title: 'Catalog /EntityRefLinks',
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(<Story />, {
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useStarredEntities } from './useStarredEntities';
|
||||
|
||||
describe('useStarredEntities', () => {
|
||||
let mockApi: StarredEntitiesApi;
|
||||
let wrapper: React.ComponentType;
|
||||
let wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
const mockEntity: Entity = {
|
||||
apiVersion: '1',
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('useStarredEntity', () => {
|
||||
toggleStarred: jest.fn(),
|
||||
starredEntitie$: jest.fn(),
|
||||
};
|
||||
let wrapper: React.ComponentType;
|
||||
let wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = (props: PropsWithChildren<{}>) => (
|
||||
|
||||
@@ -105,6 +105,13 @@ export const catalogPlugin: BackstagePlugin<
|
||||
},
|
||||
true
|
||||
>;
|
||||
createFromTemplate: ExternalRouteRef<
|
||||
{
|
||||
namespace: string;
|
||||
templateName: string;
|
||||
},
|
||||
true
|
||||
>;
|
||||
},
|
||||
CatalogInputPluginOptions
|
||||
>;
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@backstage/integration-react": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-common": "workspace:^",
|
||||
"@backstage/plugin-search-common": "workspace:^",
|
||||
"@backstage/plugin-search-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
|
||||
@@ -30,7 +30,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { viewTechDocRouteRef } from '../../routes';
|
||||
import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes';
|
||||
import { AboutCard } from './AboutCard';
|
||||
|
||||
describe('<AboutCard />', () => {
|
||||
@@ -505,4 +505,165 @@ describe('<AboutCard />', () => {
|
||||
expect(screen.getByText('View TechDocs')).toBeVisible();
|
||||
expect(screen.getByText('View TechDocs').closest('a')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders launch template link', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
kind: 'Template',
|
||||
metadata: {
|
||||
name: 'create-react-app-template',
|
||||
namespace: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
scmIntegrationsApiRef,
|
||||
ScmIntegrationsApi.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
token: '...',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
'/create/templates/:namespace/:templateName':
|
||||
createFromTemplateRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText('Launch Template')).toBeVisible();
|
||||
expect(screen.getByText('Launch Template').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'/create/templates/default/create-react-app-template',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
testName: 'entity is not a template',
|
||||
entity: {
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'create-react-app-template',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: 'apiVersion is not scaffolder.backstage.io/v1beta3',
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Template',
|
||||
metadata: {
|
||||
name: 'create-react-app-template',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
])(
|
||||
'should not render launch template link when $testName',
|
||||
async ({ entity }) => {
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
scmIntegrationsApiRef,
|
||||
ScmIntegrationsApi.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
token: '...',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
'/create/templates/:namespace/:templateName':
|
||||
createFromTemplateRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Launch Template')).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it('renders disabled launch template link when route is not bound', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
kind: 'Template',
|
||||
metadata: {
|
||||
name: 'create-react-app-template',
|
||||
namespace: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
scmIntegrationsApiRef,
|
||||
ScmIntegrationsApi.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
token: '...',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText('Launch Template')).toBeVisible();
|
||||
expect(screen.getByText('Launch Template').closest('a')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ANNOTATION_EDIT_URL,
|
||||
ANNOTATION_LOCATION,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
alertApiRef,
|
||||
errorApiRef,
|
||||
useApi,
|
||||
useApp,
|
||||
useRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
getEntitySourceLocation,
|
||||
useEntity,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -49,11 +50,13 @@ import {
|
||||
IconButton,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import CachedIcon from '@material-ui/icons/Cached';
|
||||
import DocsIcon from '@material-ui/icons/Description';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import React, { useCallback } from 'react';
|
||||
import { viewTechDocRouteRef } from '../../routes';
|
||||
|
||||
import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes';
|
||||
import { AboutContent } from './AboutContent';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -90,6 +93,7 @@ export interface AboutCardProps {
|
||||
*/
|
||||
export function AboutCard(props: AboutCardProps) {
|
||||
const { variant } = props;
|
||||
const app = useApp();
|
||||
const classes = useStyles();
|
||||
const { entity } = useEntity();
|
||||
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
|
||||
@@ -97,6 +101,7 @@ export function AboutCard(props: AboutCardProps) {
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const viewTechdocLink = useRouteRef(viewTechDocRouteRef);
|
||||
const templateRoute = useRouteRef(createFromTemplateRouteRef);
|
||||
|
||||
const entitySourceLocation = getEntitySourceLocation(
|
||||
entity,
|
||||
@@ -126,6 +131,26 @@ export function AboutCard(props: AboutCardProps) {
|
||||
}),
|
||||
};
|
||||
|
||||
const subHeaderLinks = [viewInSource, viewInTechDocs];
|
||||
|
||||
if (isTemplateEntityV1beta3(entity)) {
|
||||
const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon;
|
||||
|
||||
const launchTemplate: IconLinkVerticalProps = {
|
||||
label: 'Launch Template',
|
||||
icon: <Icon />,
|
||||
disabled: !templateRoute,
|
||||
href:
|
||||
templateRoute &&
|
||||
templateRoute({
|
||||
templateName: entity.metadata.name,
|
||||
namespace: entity.metadata.namespace || DEFAULT_NAMESPACE,
|
||||
}),
|
||||
};
|
||||
|
||||
subHeaderLinks.push(launchTemplate);
|
||||
}
|
||||
|
||||
let cardClass = '';
|
||||
if (variant === 'gridItem') {
|
||||
cardClass = classes.gridItemCard;
|
||||
@@ -179,7 +204,7 @@ export function AboutCard(props: AboutCardProps) {
|
||||
</IconButton>
|
||||
</>
|
||||
}
|
||||
subheader={<HeaderIconLinkRow links={[viewInSource, viewInTechDocs]} />}
|
||||
subheader={<HeaderIconLinkRow links={subHeaderLinks} />}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent className={cardContentClass}>
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import { DependencyOfComponentsCard } from './DependencyOfComponentsCard';
|
||||
|
||||
describe('<DependencyOfComponentsCard />', () => {
|
||||
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import { DependsOnComponentsCard } from './DependsOnComponentsCard';
|
||||
|
||||
describe('<DependsOnComponentsCard />', () => {
|
||||
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import { DependsOnResourcesCard } from './DependsOnResourcesCard';
|
||||
|
||||
describe('<DependsOnResourcesCard />', () => {
|
||||
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -28,7 +28,7 @@ import { HasComponentsCard } from './HasComponentsCard';
|
||||
|
||||
describe('<HasComponentsCard />', () => {
|
||||
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -28,7 +28,7 @@ import { HasResourcesCard } from './HasResourcesCard';
|
||||
|
||||
describe('<HasResourcesCard />', () => {
|
||||
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -28,7 +28,7 @@ import { HasSubcomponentsCard } from './HasSubcomponentsCard';
|
||||
|
||||
describe('<HasSubcomponentsCard />', () => {
|
||||
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -28,7 +28,7 @@ import { HasSystemsCard } from './HasSystemsCard';
|
||||
|
||||
describe('<HasSystemsCard />', () => {
|
||||
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
entityRouteRef,
|
||||
starredEntitiesApiRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { createComponentRouteRef, viewTechDocRouteRef } from './routes';
|
||||
import {
|
||||
createComponentRouteRef,
|
||||
createFromTemplateRouteRef,
|
||||
viewTechDocRouteRef,
|
||||
} from './routes';
|
||||
import {
|
||||
createApiFactory,
|
||||
createComponentExtension,
|
||||
@@ -77,6 +81,7 @@ export const catalogPlugin = createPlugin({
|
||||
externalRoutes: {
|
||||
createComponent: createComponentRouteRef,
|
||||
viewTechDoc: viewTechDocRouteRef,
|
||||
createFromTemplate: createFromTemplateRouteRef,
|
||||
},
|
||||
__experimentalConfigure(
|
||||
options?: CatalogInputPluginOptions,
|
||||
|
||||
@@ -30,6 +30,12 @@ export const viewTechDocRouteRef = createExternalRouteRef({
|
||||
params: ['namespace', 'kind', 'name'],
|
||||
});
|
||||
|
||||
export const createFromTemplateRouteRef = createExternalRouteRef({
|
||||
id: 'create-from-template',
|
||||
optional: true,
|
||||
params: ['namespace', 'templateName'],
|
||||
});
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'catalog',
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('<FossaCard />', () => {
|
||||
getFindingSummary: jest.fn(),
|
||||
getFindingSummaries: jest.fn(),
|
||||
};
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('<FossaPage />', () => {
|
||||
getFindingSummary: jest.fn(),
|
||||
getFindingSummaries: jest.fn(),
|
||||
};
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
+21
-27
@@ -22,11 +22,9 @@ yarn add --cwd packages/app @backstage/plugin-home
|
||||
```tsx
|
||||
import React from 'react';
|
||||
|
||||
export const HomePage = () => {
|
||||
return {
|
||||
/* TODO: Compose a Home Page here */
|
||||
};
|
||||
};
|
||||
export const homePage = (
|
||||
/* TODO: Compose a Home Page here */
|
||||
);
|
||||
```
|
||||
|
||||
2. Add a route where the homepage will live, presumably `/`.
|
||||
@@ -35,11 +33,11 @@ export const HomePage = () => {
|
||||
|
||||
```tsx
|
||||
import { HomepageCompositionRoot } from '@backstage/plugin-home';
|
||||
import { HomePage } from './components/home/HomePage';
|
||||
import { homePage } from './components/home/HomePage';
|
||||
|
||||
// ...
|
||||
<Route path="/" element={<HomepageCompositionRoot />}>
|
||||
<HomePage />
|
||||
{homePage}
|
||||
</Route>;
|
||||
// ...
|
||||
```
|
||||
@@ -72,15 +70,13 @@ import React from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { RandomJokeHomePageComponent } from '@backstage/plugin-home';
|
||||
|
||||
export const HomePage = () => {
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<RandomJokeHomePageComponent />
|
||||
</Grid>
|
||||
export const homePage = (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<RandomJokeHomePageComponent />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
</Grid>
|
||||
);
|
||||
```
|
||||
|
||||
Additionally, the App Integrator is provided an escape hatch in case the way the card is rendered does not fit their requirements. They may optionally pass the `Renderer`-prop, which will receive the `title`, `content` and optionally `actions`, `settings` and `contextProvider`, if they exist for the component. This allows the App Integrator to render the content in any way they want.
|
||||
@@ -102,18 +98,16 @@ import { HomePageSearchBar } from '@backstage/plugin-search';
|
||||
import { HomePageCalendar } from '@backstage/plugin-gcalendar';
|
||||
import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar';
|
||||
|
||||
export const HomePage = () => {
|
||||
return (
|
||||
<CustomHomepageGrid>
|
||||
// Insert the allowed widgets inside the grid
|
||||
<HomePageSearchBar />
|
||||
<HomePageRandomJoke />
|
||||
<HomePageCalendar />
|
||||
<MicrosoftCalendarCard />
|
||||
<HomePageStarredEntities />
|
||||
</CustomHomepageGrid>
|
||||
);
|
||||
};
|
||||
export const homePage = (
|
||||
<CustomHomepageGrid>
|
||||
// Insert the allowed widgets inside the grid
|
||||
<HomePageSearchBar />
|
||||
<HomePageRandomJoke />
|
||||
<HomePageCalendar />
|
||||
<MicrosoftCalendarCard />
|
||||
<HomePageStarredEntities />
|
||||
</CustomHomepageGrid>
|
||||
);
|
||||
```
|
||||
|
||||
### Creating Customizable Components
|
||||
|
||||
@@ -21,12 +21,12 @@ import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { Grid, makeStyles } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/CompanyLogo',
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
|
||||
import { Header } from '@backstage/core-components';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { ClockConfig, HeaderWorldClock } from './HeaderWorldClock';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/HeaderWorldClock',
|
||||
decorators: [(Story: ComponentType<{}>) => wrapInTestApp(<Story />)],
|
||||
decorators: [
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => wrapInTestApp(<Story />),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
entityRouteRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
|
||||
const starredEntitiesApi = new MockStarredEntitiesApi();
|
||||
starredEntitiesApi.toggleStarred('component:default/example-starred-entity');
|
||||
@@ -73,7 +73,7 @@ const mockCatalogApi = {
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/StarredEntities',
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
import { InfoCard } from '@backstage/core-components';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { ComponentAccordion } from '../../componentRenderers';
|
||||
import { HomePageToolkit } from '../../plugin';
|
||||
import { TemplateBackstageLogoIcon } from '../../assets';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/Toolkit',
|
||||
decorators: [(Story: ComponentType<{}>) => wrapInTestApp(<Story />)],
|
||||
decorators: [
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => wrapInTestApp(<Story />),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
|
||||
import { Header } from '@backstage/core-components';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { WelcomeTitle } from './WelcomeTitle';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/WelcomeTitle',
|
||||
decorators: [(Story: ComponentType<{}>) => wrapInTestApp(<Story />)],
|
||||
decorators: [
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => wrapInTestApp(<Story />),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
|
||||
Vendored
+7
-1
@@ -38,11 +38,17 @@ export interface Config {
|
||||
* instance.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
/** @visibility secret */
|
||||
apiKey: string;
|
||||
extraRequestHeaders?: Partial<{
|
||||
/** @visibility secret */
|
||||
Authorization: string;
|
||||
/** @visibility secret */
|
||||
authorization: string;
|
||||
[key: string]: string;
|
||||
}>;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { GroupProfileCard } from './GroupProfileCard';
|
||||
|
||||
const dummyDepartment = {
|
||||
@@ -74,7 +74,7 @@ export default {
|
||||
title: 'Plugins/Org/Group Profile Card',
|
||||
component: GroupProfileCard,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<Story />
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { UserProfileCard } from './UserProfileCard';
|
||||
|
||||
const dummyGroup = {
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
title: 'Plugins/Org/User Profile Card',
|
||||
component: UserProfileCard,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(<Story />, {
|
||||
mountedRoutes: {
|
||||
'/a': entityRouteRef,
|
||||
|
||||
@@ -95,6 +95,30 @@ annotations:
|
||||
pagerduty.com/integration-key: [INTEGRATION_KEY]
|
||||
```
|
||||
|
||||
### The homepage component
|
||||
|
||||
You may also add the component to the homepage of Backstage. Edit the `HomePage.tsx` file, import `PagerDutyHomepageCard` and add it to the home page. e.g.
|
||||
|
||||
```typescript jsx
|
||||
...
|
||||
export const homePage = (
|
||||
<Page themeId="home">
|
||||
...
|
||||
<Content>
|
||||
<CustomHomepageGrid config={defaultConfig}>
|
||||
...
|
||||
<PagerDutyHomepageCard
|
||||
name="My Service"
|
||||
serviceId="<service id>"
|
||||
integrationKey="<integration key>"
|
||||
readOnly={false}
|
||||
/>
|
||||
</CustomHomepageGrid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
```
|
||||
|
||||
Next, provide the [API token](https://support.pagerduty.com/docs/generating-api-keys#generating-a-general-access-rest-api-key) that the client will use to make requests to the [PagerDuty API](https://developer.pagerduty.com/docs/rest-api-v2/rest-api/).
|
||||
|
||||
Add the proxy configuration in `app-config.yaml`:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { CardExtensionProps } from '@backstage/plugin-home-react';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
@@ -14,7 +15,24 @@ import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityPagerDutyCard: (props: PagerDutyCardProps) => JSX.Element;
|
||||
export const EntityPagerDutyCard: (
|
||||
props: EntityPagerDutyCardProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type EntityPagerDutyCardProps = {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const HomePagePagerDutyCard: (
|
||||
props: CardExtensionProps<HomePagePagerDutyCardProps>,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type HomePagePagerDutyCardProps = PagerDutyEntity & {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
const isPluginApplicableToEntity: (entity: Entity) => boolean;
|
||||
@@ -31,6 +49,9 @@ export interface PagerDutyApi {
|
||||
): Promise<PagerDutyIncidentsResponse>;
|
||||
getOnCallByPolicyId(policyId: string): Promise<PagerDutyOnCallsResponse>;
|
||||
getServiceByEntity(entity: Entity): Promise<PagerDutyServiceResponse>;
|
||||
getServiceByPagerDutyEntity(
|
||||
pagerDutyEntity: PagerDutyEntity,
|
||||
): Promise<PagerDutyServiceResponse>;
|
||||
triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise<Response>;
|
||||
}
|
||||
|
||||
@@ -44,13 +65,11 @@ export type PagerDutyAssignee = {
|
||||
html_url: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const PagerDutyCard: (props: PagerDutyCardProps) => JSX.Element;
|
||||
// @public @deprecated (undocumented)
|
||||
export const PagerDutyCard: (props: EntityPagerDutyCardProps) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type PagerDutyCardProps = {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
// @public @deprecated (undocumented)
|
||||
export type PagerDutyCardProps = EntityPagerDutyCardProps;
|
||||
|
||||
// @public (undocumented)
|
||||
export type PagerDutyChangeEvent = {
|
||||
@@ -98,6 +117,10 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
// (undocumented)
|
||||
getServiceByEntity(entity: Entity): Promise<PagerDutyServiceResponse>;
|
||||
// (undocumented)
|
||||
getServiceByPagerDutyEntity(
|
||||
pagerDutyEntity: PagerDutyEntity,
|
||||
): Promise<PagerDutyServiceResponse>;
|
||||
// (undocumented)
|
||||
triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise<Response>;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,136 +15,12 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { pagerDutyPlugin, EntityPagerDutyCard } from '../src/plugin';
|
||||
import { pagerDutyApiRef } from '../src/api';
|
||||
import { PagerDutyApi, PagerDutyTriggerAlarmRequest } from '../src/api/types';
|
||||
import {
|
||||
PagerDutyIncident,
|
||||
PagerDutyChangeEvent,
|
||||
} from '../src/components/types';
|
||||
|
||||
const mockEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'backstage',
|
||||
description: 'backstage.io',
|
||||
annotations: {
|
||||
'github.com/project-slug': 'backstage/backstage',
|
||||
'pagerduty.com/service-id': 'foo',
|
||||
'pagerduty.com/integration-key': 'foo',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
lifecycle: 'production',
|
||||
type: 'website',
|
||||
owner: 'user:guest',
|
||||
},
|
||||
};
|
||||
|
||||
const mockPagerDutyApi: PagerDutyApi = {
|
||||
async getServiceByEntity(entity: Entity) {
|
||||
return {
|
||||
service: {
|
||||
name: entity.metadata.name,
|
||||
integrationKey: 'key',
|
||||
id: '123',
|
||||
html_url: 'http://service',
|
||||
escalation_policy: {
|
||||
id: '123',
|
||||
html_url: 'http://escalationpolicy',
|
||||
user: {
|
||||
id: '123',
|
||||
summary: 'summary',
|
||||
email: 'email@email.com',
|
||||
html_url: 'http://user',
|
||||
name: 'some-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async getIncidentsByServiceId(serviceId: string) {
|
||||
const incident = (title: string) => {
|
||||
return {
|
||||
id: '123',
|
||||
title: title,
|
||||
status: 'acknowledged',
|
||||
html_url: 'http://incident',
|
||||
assignments: [
|
||||
{
|
||||
assignee: {
|
||||
id: '123',
|
||||
summary: 'Jane Doe',
|
||||
html_url: 'http://assignee',
|
||||
},
|
||||
},
|
||||
],
|
||||
serviceId: serviceId,
|
||||
created_at: '2015-10-06T21:30:42Z',
|
||||
} as PagerDutyIncident;
|
||||
};
|
||||
|
||||
return {
|
||||
incidents: [
|
||||
incident('Some Alerting Incident'),
|
||||
incident('Another Alerting Incident'),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
async getChangeEventsByServiceId(serviceId: string) {
|
||||
const changeEvent = (description: string) => {
|
||||
return {
|
||||
id: serviceId,
|
||||
source: 'some-source',
|
||||
html_url: 'http://changeevent',
|
||||
links: [
|
||||
{
|
||||
href: 'http://link',
|
||||
text: 'link text',
|
||||
},
|
||||
],
|
||||
summary: description,
|
||||
timestamp: '2018-10-06T21:30:42Z',
|
||||
} as PagerDutyChangeEvent;
|
||||
};
|
||||
|
||||
return {
|
||||
change_events: [
|
||||
changeEvent('us-east-1 deployment'),
|
||||
changeEvent('us-west-2 deployment'),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
async getOnCallByPolicyId() {
|
||||
const oncall = (name: string, escalation: number) => {
|
||||
return {
|
||||
user: {
|
||||
id: '123',
|
||||
name: name,
|
||||
html_url: 'http://assignee',
|
||||
summary: 'summary',
|
||||
email: 'email@email.com',
|
||||
},
|
||||
escalation_level: escalation,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
oncalls: [oncall('Jane Doe', 1), oncall('John Doe', 2)],
|
||||
};
|
||||
},
|
||||
|
||||
async triggerAlarm(request: PagerDutyTriggerAlarmRequest) {
|
||||
return new Response(request.description);
|
||||
},
|
||||
};
|
||||
import { mockPagerDutyApi } from './mockPagerDutyApi';
|
||||
import { mockEntity } from './mockEntity';
|
||||
|
||||
createDevApp()
|
||||
.registerApi({
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const mockEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'backstage',
|
||||
description: 'backstage.io',
|
||||
annotations: {
|
||||
'github.com/project-slug': 'backstage/backstage',
|
||||
'pagerduty.com/service-id': 'foo',
|
||||
'pagerduty.com/integration-key': 'foo',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
lifecycle: 'production',
|
||||
type: 'website',
|
||||
owner: 'user:guest',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
PagerDutyApi,
|
||||
PagerDutyChangeEvent,
|
||||
PagerDutyEntity,
|
||||
PagerDutyIncident,
|
||||
PagerDutyTriggerAlarmRequest,
|
||||
} from '../src';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const mockPagerDutyApi: PagerDutyApi = {
|
||||
async getServiceByPagerDutyEntity(pagerDutyEntity: PagerDutyEntity) {
|
||||
return {
|
||||
service: {
|
||||
name: pagerDutyEntity.name,
|
||||
integrationKey: 'key',
|
||||
id: '123',
|
||||
html_url: 'http://service',
|
||||
escalation_policy: {
|
||||
id: '123',
|
||||
html_url: 'http://escalationpolicy',
|
||||
user: {
|
||||
id: '123',
|
||||
summary: 'summary',
|
||||
email: 'email@email.com',
|
||||
html_url: 'http://user',
|
||||
name: 'some-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async getServiceByEntity(entity: Entity) {
|
||||
return {
|
||||
service: {
|
||||
name: entity.metadata.name,
|
||||
integrationKey: 'key',
|
||||
id: '123',
|
||||
html_url: 'http://service',
|
||||
escalation_policy: {
|
||||
id: '123',
|
||||
html_url: 'http://escalationpolicy',
|
||||
user: {
|
||||
id: '123',
|
||||
summary: 'summary',
|
||||
email: 'email@email.com',
|
||||
html_url: 'http://user',
|
||||
name: 'some-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async getIncidentsByServiceId(serviceId: string) {
|
||||
const incident = (title: string) => {
|
||||
return {
|
||||
id: '123',
|
||||
title: title,
|
||||
status: 'acknowledged',
|
||||
html_url: 'http://incident',
|
||||
assignments: [
|
||||
{
|
||||
assignee: {
|
||||
id: '123',
|
||||
summary: 'Jane Doe',
|
||||
html_url: 'http://assignee',
|
||||
},
|
||||
},
|
||||
],
|
||||
serviceId: serviceId,
|
||||
created_at: '2015-10-06T21:30:42Z',
|
||||
} as PagerDutyIncident;
|
||||
};
|
||||
|
||||
return {
|
||||
incidents: [
|
||||
incident('Some Alerting Incident'),
|
||||
incident('Another Alerting Incident'),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
async getChangeEventsByServiceId(serviceId: string) {
|
||||
const changeEvent = (description: string) => {
|
||||
return {
|
||||
id: serviceId,
|
||||
source: 'some-source',
|
||||
html_url: 'http://changeevent',
|
||||
links: [
|
||||
{
|
||||
href: 'http://link',
|
||||
text: 'link text',
|
||||
},
|
||||
],
|
||||
summary: description,
|
||||
timestamp: '2018-10-06T21:30:42Z',
|
||||
} as PagerDutyChangeEvent;
|
||||
};
|
||||
|
||||
return {
|
||||
change_events: [
|
||||
changeEvent('us-east-1 deployment'),
|
||||
changeEvent('us-west-2 deployment'),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
async getOnCallByPolicyId() {
|
||||
const oncall = (name: string, escalation: number) => {
|
||||
return {
|
||||
user: {
|
||||
id: '123',
|
||||
name: name,
|
||||
html_url: 'http://assignee',
|
||||
summary: 'summary',
|
||||
email: 'email@email.com',
|
||||
},
|
||||
escalation_level: escalation,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
oncalls: [oncall('Jane Doe', 1), oncall('John Doe', 2)],
|
||||
};
|
||||
},
|
||||
|
||||
async triggerAlarm(request: PagerDutyTriggerAlarmRequest) {
|
||||
return new Response(request.description);
|
||||
},
|
||||
};
|
||||
@@ -38,6 +38,7 @@
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-home-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -30,6 +30,7 @@ import { createApiRef, ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { getPagerDutyEntity } from '../components/pagerDutyEntity';
|
||||
import { PagerDutyEntity } from '../types';
|
||||
|
||||
/** @public */
|
||||
export class UnauthorizedError extends Error {}
|
||||
@@ -63,8 +64,10 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
|
||||
constructor(private readonly config: PagerDutyClientApiConfig) {}
|
||||
|
||||
async getServiceByEntity(entity: Entity): Promise<PagerDutyServiceResponse> {
|
||||
const { integrationKey, serviceId } = getPagerDutyEntity(entity);
|
||||
async getServiceByPagerDutyEntity(
|
||||
pagerDutyEntity: PagerDutyEntity,
|
||||
): Promise<PagerDutyServiceResponse> {
|
||||
const { integrationKey, serviceId } = pagerDutyEntity;
|
||||
|
||||
let response: PagerDutyServiceResponse;
|
||||
let url: string;
|
||||
@@ -92,6 +95,9 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
return response;
|
||||
}
|
||||
|
||||
async getServiceByEntity(entity: Entity): Promise<PagerDutyServiceResponse> {
|
||||
return await this.getServiceByPagerDutyEntity(getPagerDutyEntity(entity));
|
||||
}
|
||||
async getIncidentsByServiceId(
|
||||
serviceId: string,
|
||||
): Promise<PagerDutyIncidentsResponse> {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../components/types';
|
||||
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { PagerDutyEntity } from '../types';
|
||||
|
||||
export type PagerDutyServicesResponse = {
|
||||
services: PagerDutyService[];
|
||||
@@ -57,6 +58,14 @@ export type PagerDutyTriggerAlarmRequest = {
|
||||
|
||||
/** @public */
|
||||
export interface PagerDutyApi {
|
||||
/**
|
||||
* Fetches the service for the provided pager duty Entity.
|
||||
*
|
||||
*/
|
||||
getServiceByPagerDutyEntity(
|
||||
pagerDutyEntity: PagerDutyEntity,
|
||||
): Promise<PagerDutyServiceResponse>;
|
||||
|
||||
/**
|
||||
* Fetches the service for the provided Entity.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { render, waitFor, fireEvent, act } from '@testing-library/react';
|
||||
import {
|
||||
EntityPagerDutyCard,
|
||||
isPluginApplicableToEntity,
|
||||
} from '../EntityPagerDutyCard';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api';
|
||||
import { PagerDutyService, PagerDutyUser } from '../types';
|
||||
|
||||
import { alertApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const entityWithoutAnnotations: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {},
|
||||
},
|
||||
};
|
||||
|
||||
const entityWithServiceId: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/service-id': 'def456',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const entityWithAllAnnotations: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
'pagerduty.com/service-id': 'def456',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const user: PagerDutyUser = {
|
||||
name: 'person1',
|
||||
id: 'p1',
|
||||
summary: 'person1',
|
||||
email: 'person1@example.com',
|
||||
html_url: 'http://a.com/id1',
|
||||
};
|
||||
|
||||
const service: PagerDutyService = {
|
||||
id: 'def456',
|
||||
name: 'pagerduty-name',
|
||||
html_url: 'www.example.com',
|
||||
escalation_policy: {
|
||||
id: 'def',
|
||||
user: user,
|
||||
html_url: 'http://a.com/id1',
|
||||
},
|
||||
integrationKey: 'abc123',
|
||||
};
|
||||
|
||||
const mockPagerDutyApi: Partial<PagerDutyClient> = {
|
||||
getServiceByEntity: async () => ({ service }),
|
||||
getServiceByPagerDutyEntity: async () => ({ service }),
|
||||
getOnCallByPolicyId: async () => ({ oncalls: [] }),
|
||||
getIncidentsByServiceId: async () => ({ incidents: [] }),
|
||||
};
|
||||
|
||||
const apis = TestApiRegistry.from(
|
||||
[pagerDutyApiRef, mockPagerDutyApi],
|
||||
[alertApiRef, {}],
|
||||
);
|
||||
|
||||
describe('isPluginApplicableToEntity', () => {
|
||||
describe('when entity has no annotations', () => {
|
||||
it('returns false', () => {
|
||||
expect(isPluginApplicableToEntity(entityWithoutAnnotations)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entity has the pagerduty.com/integration-key annotation', () => {
|
||||
it('returns true', () => {
|
||||
expect(isPluginApplicableToEntity(entity)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entity has the pagerduty.com/service-id annotation', () => {
|
||||
it('returns true', () => {
|
||||
expect(isPluginApplicableToEntity(entityWithServiceId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entity has all annotations', () => {
|
||||
it('returns true', () => {
|
||||
expect(isPluginApplicableToEntity(entityWithAllAnnotations)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('EntityPagerDutyCard', () => {
|
||||
it('Render pagerduty', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Service Directory')).toBeInTheDocument();
|
||||
expect(getByText('Create Incident')).toBeInTheDocument();
|
||||
expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
|
||||
expect(getByText('Empty escalation policy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Handles custom error for missing token', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new UnauthorizedError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Missing or invalid PagerDuty Token')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Handles custom NotFoundError', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new NotFoundError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles general error', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('An error occurred'));
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
|
||||
expect(
|
||||
getByText(
|
||||
'Error encountered while fetching information. An error occurred',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the dialog when trigger button is clicked', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId, getByRole } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Service Directory')).toBeInTheDocument();
|
||||
|
||||
const triggerLink = getByText('Create Incident');
|
||||
await act(async () => {
|
||||
fireEvent.click(triggerLink);
|
||||
});
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when entity has the pagerduty.com/service-id annotation', () => {
|
||||
it('Renders PagerDuty service information', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Service Directory')).toBeInTheDocument();
|
||||
expect(getByText('Create Incident')).toBeInTheDocument();
|
||||
expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
|
||||
expect(getByText('Empty escalation policy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Handles custom error for missing token', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new UnauthorizedError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(
|
||||
getByText('Missing or invalid PagerDuty Token'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Handles custom NotFoundError', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new NotFoundError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles general error', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('An error occurred'));
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
|
||||
expect(
|
||||
getByText(
|
||||
'Error encountered while fetching information. An error occurred',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the Create Incident button', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { queryByTestId, getByTitle } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(
|
||||
getByTitle('Must provide an integration-key to create incidents')
|
||||
.className,
|
||||
).toMatch('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entity has all annotations', () => {
|
||||
it('queries by integration key', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithAllAnnotations}>
|
||||
<EntityPagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Service Directory')).toBeInTheDocument();
|
||||
expect(getByText('Create Incident')).toBeInTheDocument();
|
||||
expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
|
||||
expect(getByText('Empty escalation policy')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entity has all annotations but the plugin has been configured to be "read only"', () => {
|
||||
it('queries by integration key but does not render the "Create Incident" button', async () => {
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithAllAnnotations}>
|
||||
<EntityPagerDutyCard readOnly />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Service Directory')).toBeInTheDocument();
|
||||
expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
|
||||
expect(getByText('Empty escalation policy')).toBeInTheDocument();
|
||||
expect(() => getByText('Create Incident')).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { getPagerDutyEntity } from '../pagerDutyEntity';
|
||||
import { PagerDutyCard } from '../PagerDutyCard';
|
||||
|
||||
/** @public */
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(
|
||||
entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY] ||
|
||||
entity.metadata.annotations?.[PAGERDUTY_SERVICE_ID],
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export type EntityPagerDutyCardProps = {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const EntityPagerDutyCard = (props: EntityPagerDutyCardProps) => {
|
||||
const { readOnly } = props;
|
||||
const { entity } = useEntity();
|
||||
const pagerDutyEntity = getPagerDutyEntity(entity);
|
||||
return <PagerDutyCard {...pagerDutyEntity} readOnly={readOnly} />;
|
||||
};
|
||||
@@ -21,7 +21,7 @@ export const ServiceNotFoundError = () => (
|
||||
<EmptyState
|
||||
missing="data"
|
||||
title="PagerDuty Service Not Found"
|
||||
description="A service could not be found within PagerDuty based on the provided annotations. Please verify your annotation details."
|
||||
description="A service could not be found within PagerDuty based on the provided service id. Please verify your configuration."
|
||||
action={
|
||||
<Button
|
||||
color="primary"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { PagerDutyEntity } from '../../types';
|
||||
import { PagerDutyCard } from '../PagerDutyCard';
|
||||
|
||||
/** @public */
|
||||
export type HomePagePagerDutyCardProps = PagerDutyEntity & {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const Content = (props: HomePagePagerDutyCardProps) => {
|
||||
return <PagerDutyCard {...props} />;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export { Content } from './Content';
|
||||
export type { HomePagePagerDutyCardProps } from './Content';
|
||||
@@ -15,9 +15,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, waitFor, fireEvent, act } from '@testing-library/react';
|
||||
import { PagerDutyCard, isPluginApplicableToEntity } from '../PagerDutyCard';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { PagerDutyCard } from '../PagerDutyCard';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api';
|
||||
@@ -26,49 +24,6 @@ import { PagerDutyService, PagerDutyUser } from '../types';
|
||||
import { alertApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const entityWithoutAnnotations: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {},
|
||||
},
|
||||
};
|
||||
|
||||
const entityWithServiceId: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/service-id': 'def456',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const entityWithAllAnnotations: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
'pagerduty.com/service-id': 'def456',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const user: PagerDutyUser = {
|
||||
name: 'person1',
|
||||
id: 'p1',
|
||||
@@ -100,44 +55,16 @@ const apis = TestApiRegistry.from(
|
||||
[alertApiRef, {}],
|
||||
);
|
||||
|
||||
describe('isPluginApplicableToEntity', () => {
|
||||
describe('when entity has no annotations', () => {
|
||||
it('returns false', () => {
|
||||
expect(isPluginApplicableToEntity(entityWithoutAnnotations)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entity has the pagerduty.com/integration-key annotation', () => {
|
||||
it('returns true', () => {
|
||||
expect(isPluginApplicableToEntity(entity)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entity has the pagerduty.com/service-id annotation', () => {
|
||||
it('returns true', () => {
|
||||
expect(isPluginApplicableToEntity(entityWithServiceId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entity has all annotations', () => {
|
||||
it('returns true', () => {
|
||||
expect(isPluginApplicableToEntity(entityWithAllAnnotations)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PageDutyCard', () => {
|
||||
describe('PagerDutyCard', () => {
|
||||
it('Render pagerduty', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard name="blah" integrationKey="abc123" />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -149,16 +76,14 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('Handles custom error for missing token', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new UnauthorizedError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard name="blah" integrationKey="abc123" />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -167,16 +92,14 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('Handles custom NotFoundError', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new NotFoundError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard name="blah" integrationKey="abc123" />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -185,15 +108,13 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('handles general error', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('An error occurred'));
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard name="blah" integrationKey="abc123" />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -207,16 +128,14 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('opens the dialog when trigger button is clicked', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId, getByRole } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard name="blah" integrationKey="abc123" />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -232,16 +151,14 @@ describe('PageDutyCard', () => {
|
||||
|
||||
describe('when entity has the pagerduty.com/service-id annotation', () => {
|
||||
it('Renders PagerDuty service information', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard name="blah" integrationKey="abc123" />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -253,16 +170,18 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('Handles custom error for missing token', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new UnauthorizedError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard
|
||||
name="blah"
|
||||
integrationKey="abc123"
|
||||
serviceId="def123"
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -273,16 +192,18 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('Handles custom NotFoundError', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new NotFoundError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard
|
||||
name="blah"
|
||||
integrationKey="abc123"
|
||||
serviceId="def123"
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -291,15 +212,17 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('handles general error', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('An error occurred'));
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard
|
||||
name="blah"
|
||||
integrationKey="abc123"
|
||||
serviceId="def123"
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -313,16 +236,14 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('disables the Create Incident button', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { queryByTestId, getByTitle } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard name="blah" serviceId="def123" />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -336,16 +257,18 @@ describe('PageDutyCard', () => {
|
||||
|
||||
describe('when entity has all annotations', () => {
|
||||
it('queries by integration key', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithAllAnnotations}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard
|
||||
name="blah"
|
||||
integrationKey="abc123"
|
||||
serviceId="def123"
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
@@ -359,16 +282,19 @@ describe('PageDutyCard', () => {
|
||||
|
||||
describe('when entity has all annotations but the plugin has been configured to be "read only"', () => {
|
||||
it('queries by integration key but does not render the "Create Incident" button', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
mockPagerDutyApi.getServiceByPagerDutyEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithAllAnnotations}>
|
||||
<PagerDutyCard readOnly />
|
||||
</EntityProvider>
|
||||
<PagerDutyCard
|
||||
name="blah"
|
||||
integrationKey="abc123"
|
||||
serviceId="def123"
|
||||
readOnly
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { ReactNode, useState, useCallback } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Card, CardHeader, Divider, CardContent } from '@material-ui/core';
|
||||
import { Incidents } from '../Incident';
|
||||
import { EscalationPolicy } from '../Escalation';
|
||||
@@ -25,7 +24,6 @@ import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
|
||||
import { MissingTokenError, ServiceNotFoundError } from '../Errors';
|
||||
import WebIcon from '@material-ui/icons/Web';
|
||||
import DateRangeIcon from '@material-ui/icons/DateRange';
|
||||
import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants';
|
||||
import { TriggerDialog } from '../TriggerDialog';
|
||||
import { ChangeEvents } from '../ChangeEvents';
|
||||
|
||||
@@ -39,30 +37,20 @@ import {
|
||||
CardTab,
|
||||
InfoCard,
|
||||
} from '@backstage/core-components';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { getPagerDutyEntity } from '../pagerDutyEntity';
|
||||
import { PagerDutyEntity } from '../../types';
|
||||
|
||||
const BasicCard = ({ children }: { children: ReactNode }) => (
|
||||
<InfoCard title="PagerDuty">{children}</InfoCard>
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(
|
||||
entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY] ||
|
||||
entity.metadata.annotations?.[PAGERDUTY_SERVICE_ID],
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export type PagerDutyCardProps = {
|
||||
export type PagerDutyCardProps = PagerDutyEntity & {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const PagerDutyCard = (props: PagerDutyCardProps) => {
|
||||
const { readOnly } = props;
|
||||
const { entity } = useEntity();
|
||||
const pagerDutyEntity = getPagerDutyEntity(entity);
|
||||
const { readOnly, integrationKey, name } = props;
|
||||
const api = useApi(pagerDutyApiRef);
|
||||
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
|
||||
const [refreshChangeEvents, setRefreshChangeEvents] =
|
||||
@@ -86,7 +74,9 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => {
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
const { service: foundService } = await api.getServiceByEntity(entity);
|
||||
const { service: foundService } = await api.getServiceByPagerDutyEntity(
|
||||
props,
|
||||
);
|
||||
|
||||
return {
|
||||
id: foundService.id,
|
||||
@@ -137,7 +127,7 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => {
|
||||
* There is no guarantee the current user entity has a valid email association, so instead just
|
||||
* only allow triggering incidents when an integration key is present.
|
||||
*/
|
||||
const createIncidentDisabled = !pagerDutyEntity.integrationKey;
|
||||
const createIncidentDisabled = !integrationKey;
|
||||
const triggerLink: IconLinkVerticalProps = {
|
||||
label: 'Create Incident',
|
||||
onClick: showDialog,
|
||||
@@ -195,6 +185,8 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => {
|
||||
showDialog={dialogShown}
|
||||
handleDialog={hideDialog}
|
||||
onIncidentCreated={handleRefresh}
|
||||
name={name}
|
||||
integrationKey={integrationKey}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -33,7 +33,7 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
/** @public */
|
||||
export function TriggerButton(props: { children?: ReactNode }) {
|
||||
const { buttonStyle } = useStyles();
|
||||
const { integrationKey } = usePagerdutyEntity();
|
||||
const { integrationKey, name } = usePagerdutyEntity();
|
||||
const [dialogShown, setDialogShown] = useState<boolean>(false);
|
||||
|
||||
const showDialog = useCallback(() => {
|
||||
@@ -57,7 +57,12 @@ export function TriggerButton(props: { children?: ReactNode }) {
|
||||
: 'Missing integration key'}
|
||||
</Button>
|
||||
{integrationKey && (
|
||||
<TriggerDialog showDialog={dialogShown} handleDialog={hideDialog} />
|
||||
<TriggerDialog
|
||||
showDialog={dialogShown}
|
||||
handleDialog={hideDialog}
|
||||
integrationKey={integrationKey}
|
||||
name={name}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -17,8 +17,6 @@ import React from 'react';
|
||||
import { fireEvent, act } from '@testing-library/react';
|
||||
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
|
||||
import { pagerDutyApiRef } from '../../api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { TriggerDialog } from './TriggerDialog';
|
||||
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
@@ -49,26 +47,15 @@ describe('TriggerDialog', () => {
|
||||
);
|
||||
|
||||
it('open the dialog and trigger an alarm', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { getByText, getByRole, getByTestId } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<TriggerDialog
|
||||
showDialog
|
||||
handleDialog={() => {}}
|
||||
onIncidentCreated={() => {}}
|
||||
/>
|
||||
</EntityProvider>
|
||||
<TriggerDialog
|
||||
integrationKey="abc123"
|
||||
name="pagerduty-test"
|
||||
showDialog
|
||||
handleDialog={() => {}}
|
||||
onIncidentCreated={() => {}}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
@@ -89,8 +76,7 @@ describe('TriggerDialog', () => {
|
||||
});
|
||||
expect(mockTriggerAlarmFn).toHaveBeenCalled();
|
||||
expect(mockTriggerAlarmFn).toHaveBeenCalledWith({
|
||||
integrationKey:
|
||||
entity!.metadata!.annotations!['pagerduty.com/integration-key'],
|
||||
integrationKey: 'abc123',
|
||||
source: window.location.toString(),
|
||||
description,
|
||||
userName: 'guest',
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
||||
import { pagerDutyApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { usePagerdutyEntity } from '../../hooks';
|
||||
import {
|
||||
useApi,
|
||||
alertApiRef,
|
||||
@@ -40,14 +39,17 @@ type Props = {
|
||||
showDialog: boolean;
|
||||
handleDialog: () => void;
|
||||
onIncidentCreated?: () => void;
|
||||
name: string;
|
||||
integrationKey: string;
|
||||
};
|
||||
|
||||
export const TriggerDialog = ({
|
||||
showDialog,
|
||||
handleDialog,
|
||||
onIncidentCreated: onIncidentCreated,
|
||||
name,
|
||||
integrationKey,
|
||||
}: Props) => {
|
||||
const { name, integrationKey } = usePagerdutyEntity();
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const api = useApi(pagerDutyApiRef);
|
||||
|
||||
@@ -23,12 +23,13 @@ export type {
|
||||
PagerDutyUser,
|
||||
} from './types';
|
||||
|
||||
export type { PagerDutyCardProps } from './PagerDutyCard';
|
||||
export type { EntityPagerDutyCardProps } from './EntityPagerDutyCard';
|
||||
export type { HomePagePagerDutyCardProps } from './HomePagePagerDutyCard';
|
||||
|
||||
export {
|
||||
isPluginApplicableToEntity,
|
||||
isPluginApplicableToEntity as isPagerDutyAvailable,
|
||||
PagerDutyCard,
|
||||
} from './PagerDutyCard';
|
||||
EntityPagerDutyCard,
|
||||
} from './EntityPagerDutyCard';
|
||||
|
||||
export { TriggerButton } from './TriggerButton';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 { EntityPagerDutyCardProps, EntityPagerDutyCard } from './components';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please use EntityPagerDutyCard
|
||||
*/
|
||||
export const PagerDutyCard = EntityPagerDutyCard;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please use EntityPagerDutyCardProps
|
||||
*/
|
||||
export type PagerDutyCardProps = EntityPagerDutyCardProps;
|
||||
@@ -24,9 +24,12 @@ export {
|
||||
pagerDutyPlugin,
|
||||
pagerDutyPlugin as plugin,
|
||||
EntityPagerDutyCard,
|
||||
HomePagePagerDutyCard,
|
||||
} from './plugin';
|
||||
|
||||
export * from './components';
|
||||
|
||||
export * from './api';
|
||||
export * from './deprecated';
|
||||
|
||||
export type { PagerDutyEntity } from './types';
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
configApiRef,
|
||||
createComponentExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createCardExtension } from '@backstage/plugin-home-react';
|
||||
import { HomePagePagerDutyCardProps } from './components/HomePagePagerDutyCard/Content';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'pagerduty',
|
||||
@@ -51,7 +53,38 @@ export const EntityPagerDutyCard = pagerDutyPlugin.provide(
|
||||
name: 'EntityPagerDutyCard',
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/PagerDutyCard').then(m => m.PagerDutyCard),
|
||||
import('./components/EntityPagerDutyCard').then(
|
||||
m => m.EntityPagerDutyCard,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const HomePagePagerDutyCard = pagerDutyPlugin.provide(
|
||||
createCardExtension<HomePagePagerDutyCardProps>({
|
||||
name: 'HomePagePagerDutyCard',
|
||||
title: 'PagerDuty Homepage Card',
|
||||
components: () => import('./components/HomePagePagerDutyCard'),
|
||||
settings: {
|
||||
schema: {
|
||||
title: 'PagerDuty',
|
||||
type: 'object',
|
||||
properties: {
|
||||
integrationKey: {
|
||||
title: 'PagerDuty integration key',
|
||||
type: 'string',
|
||||
},
|
||||
serviceId: {
|
||||
title: 'PagerDuty service id',
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
title: 'PagerDuty service name',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Vendored
+11
-1
@@ -31,7 +31,17 @@ export interface Config {
|
||||
/**
|
||||
* Object with extra headers to be added to target requests.
|
||||
*/
|
||||
headers?: { [key: string]: string };
|
||||
headers?: Partial<{
|
||||
/** @visibility secret */
|
||||
Authorization: string;
|
||||
/** @visibility secret */
|
||||
authorization: string;
|
||||
/** @visibility secret */
|
||||
'X-Api-Key': string;
|
||||
/** @visibility secret */
|
||||
'x-api-key': string;
|
||||
[key: string]: string;
|
||||
}>;
|
||||
/**
|
||||
* Changes the origin of the host header to the target URL. Default: true.
|
||||
*/
|
||||
|
||||
@@ -26,8 +26,8 @@ export const createGitlabProjectAccessTokenAction: (options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) => TemplateAction<
|
||||
{
|
||||
projectId: string | number;
|
||||
repoUrl: string;
|
||||
projectId: string | number;
|
||||
token?: string | undefined;
|
||||
name?: string | undefined;
|
||||
accessLevel?: number | undefined;
|
||||
@@ -44,8 +44,8 @@ export const createGitlabProjectDeployTokenAction: (options: {
|
||||
}) => TemplateAction<
|
||||
{
|
||||
name: string;
|
||||
projectId: string | number;
|
||||
repoUrl: string;
|
||||
projectId: string | number;
|
||||
token?: string | undefined;
|
||||
username?: string | undefined;
|
||||
scopes?: string[] | undefined;
|
||||
@@ -63,8 +63,8 @@ export const createGitlabProjectVariableAction: (options: {
|
||||
{
|
||||
key: string;
|
||||
value: string;
|
||||
projectId: string | number;
|
||||
repoUrl: string;
|
||||
projectId: string | number;
|
||||
variableType: string;
|
||||
token?: string | undefined;
|
||||
variableProtected?: boolean | undefined;
|
||||
|
||||
@@ -207,7 +207,9 @@ export interface ScaffolderGetIntegrationsListResponse {
|
||||
}
|
||||
|
||||
// @public
|
||||
export const ScaffolderLayouts: React.ComponentType;
|
||||
export const ScaffolderLayouts: React_2.ComponentType<
|
||||
React_2.PropsWithChildren<{}>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ScaffolderOutputLink = {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from './keys';
|
||||
import { attachComponentData, Extension } from '@backstage/core-plugin-api';
|
||||
import type { FormProps as SchemaFormProps } from '@rjsf/core-v5';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* The field template from `@rjsf/core` which is a react component that gets passed `@rjsf/core` field related props.
|
||||
@@ -67,7 +68,8 @@ export function createScaffolderLayout<TInputProps = unknown>(
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ScaffolderLayouts: React.ComponentType = (): JSX.Element | null =>
|
||||
null;
|
||||
export const ScaffolderLayouts: React.ComponentType<
|
||||
React.PropsWithChildren<{}>
|
||||
> = (): JSX.Element | null => null;
|
||||
|
||||
attachComponentData(ScaffolderLayouts, LAYOUTS_WRAPPER_KEY, true);
|
||||
|
||||
@@ -27,7 +27,7 @@ export type NextRouterProps = {
|
||||
TemplateCardComponent?: React_2.ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>;
|
||||
TaskPageComponent?: React_2.ComponentType<{}>;
|
||||
TaskPageComponent?: React_2.ComponentType<PropsWithChildren<{}>>;
|
||||
TemplateOutputsComponent?: React_2.ComponentType<{
|
||||
output?: ScaffolderTaskOutput;
|
||||
}>;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-
|
||||
import { LogEvent as LogEvent_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { PathParams } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
@@ -382,7 +383,7 @@ export type RouterProps = {
|
||||
template: TemplateEntityV1beta3;
|
||||
}>
|
||||
| undefined;
|
||||
TaskPageComponent?: ComponentType<{}>;
|
||||
TaskPageComponent?: ComponentType<PropsWithChildren<{}>>;
|
||||
};
|
||||
groups?: Array<{
|
||||
title?: React_2.ReactNode;
|
||||
@@ -466,7 +467,9 @@ export type ScaffolderGetIntegrationsListResponse =
|
||||
ScaffolderGetIntegrationsListResponse_2;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const ScaffolderLayouts: ComponentType<{}>;
|
||||
export const ScaffolderLayouts: ComponentType<{
|
||||
children?: ReactNode;
|
||||
}>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type ScaffolderOutputlink = ScaffolderOutputLink;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType, useEffect } from 'react';
|
||||
import React, { ComponentType, useEffect, PropsWithChildren } from 'react';
|
||||
import { Navigate, Route, Routes, useOutlet } from 'react-router-dom';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
@@ -52,7 +52,7 @@ export type RouterProps = {
|
||||
TemplateCardComponent?:
|
||||
| ComponentType<{ template: TemplateEntityV1beta3 }>
|
||||
| undefined;
|
||||
TaskPageComponent?: ComponentType<{}>;
|
||||
TaskPageComponent?: ComponentType<PropsWithChildren<{}>>;
|
||||
};
|
||||
groups?: Array<{
|
||||
title?: React.ReactNode;
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('<EntityPicker />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
entities = [
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('<OwnerPicker />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
entities = [
|
||||
|
||||
@@ -58,7 +58,7 @@ export type NextRouterProps = {
|
||||
TemplateCardComponent?: React.ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>;
|
||||
TaskPageComponent?: React.ComponentType<{}>;
|
||||
TaskPageComponent?: React.ComponentType<PropsWithChildren<{}>>;
|
||||
TemplateOutputsComponent?: React.ComponentType<{
|
||||
output?: ScaffolderTaskOutput;
|
||||
}>;
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
|
||||
import { Grid } from '@material-ui/core';
|
||||
import LabelIcon from '@material-ui/icons/Label';
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchAutocomplete',
|
||||
component: SearchAutocomplete,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchAutocompleteDefaultOption',
|
||||
component: SearchAutocompleteDefaultOption,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { Grid, makeStyles } from '@material-ui/core';
|
||||
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchBar',
|
||||
component: SearchBar,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { Grid, Paper } from '@material-ui/core';
|
||||
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchFilter',
|
||||
component: SearchFilter,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchPagination',
|
||||
component: SearchPagination,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
|
||||
import { List, ListItem } from '@material-ui/core';
|
||||
import DefaultIcon from '@material-ui/icons/InsertDriveFile';
|
||||
@@ -70,7 +70,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchResult',
|
||||
component: SearchResult,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<SearchContextProvider>
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType, useCallback, useState } from 'react';
|
||||
import React, {
|
||||
ComponentType,
|
||||
useCallback,
|
||||
useState,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
Grid,
|
||||
@@ -72,7 +77,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchResultGroup',
|
||||
component: SearchResultGroup,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<Grid container direction="row">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType, useState } from 'react';
|
||||
import React, { ComponentType, useState, PropsWithChildren } from 'react';
|
||||
|
||||
import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
|
||||
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchResultList',
|
||||
component: SearchResultList,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<Grid container direction="row">
|
||||
|
||||
@@ -18,12 +18,12 @@ import { rootRouteRef, HomePageSearchBar } from '../../plugin';
|
||||
import { searchApiRef } from '@backstage/plugin-search-react';
|
||||
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { Grid, makeStyles } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/SearchBar',
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<>
|
||||
<TestApiProvider
|
||||
|
||||
@@ -35,8 +35,7 @@ import {
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { rootRouteRef } from '../../plugin';
|
||||
import { SearchType } from '../SearchType';
|
||||
import { SearchModal } from './SearchModal';
|
||||
@@ -75,7 +74,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchModal',
|
||||
component: SearchModal,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[[searchApiRef, new MockSearchApi(mockResults)]]}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Grid, Paper } from '@material-ui/core';
|
||||
import CatalogIcon from '@material-ui/icons/MenuBook';
|
||||
import DocsIcon from '@material-ui/icons/Description';
|
||||
import UsersGroupsIcon from '@material-ui/icons/Person';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { SearchType } from './SearchType';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
title: 'Plugins/Search/SearchType',
|
||||
component: SearchType,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { StackOverflowIcon } from '../../icons';
|
||||
import { stackOverflowApiRef } from '../../api';
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
title: 'Plugins/Home/Components/StackOverflow',
|
||||
component: HomePageStackOverflowQuestions,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) =>
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<>
|
||||
<TestApiProvider
|
||||
|
||||
+4
-2
@@ -16,13 +16,15 @@
|
||||
|
||||
import { StackOverflowSearchResultListItem } from '../../plugin';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import React, { ComponentType } from 'react';
|
||||
import React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { StackOverflowIcon } from '../../icons';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Search/StackOverflowResultListItem',
|
||||
component: StackOverflowSearchResultListItem,
|
||||
decorators: [(Story: ComponentType<{}>) => wrapInTestApp(<Story />)],
|
||||
decorators: [
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => wrapInTestApp(<Story />),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from './useReaderState';
|
||||
|
||||
describe('useReaderState', () => {
|
||||
let Wrapper: React.ComponentType;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
const techdocsStorageApi: jest.Mocked<typeof techdocsStorageApiRef.T> = {
|
||||
getApiOrigin: jest.fn(),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
@@ -31,7 +31,7 @@ const configApiMock: ConfigApi = new ConfigReader({
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper: FC = ({ children }) => (
|
||||
const wrapper: FC<PropsWithChildren<{}>> = ({ children }) => (
|
||||
<TestApiProvider apis={[[configApiRef, configApiMock]]}>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
|
||||
Reference in New Issue
Block a user