rename azure-functions to azure-sites

add drop down for actions
show kind on table

Signed-off-by: Wesley Pattison <wesley.pattison@friss.com>
This commit is contained in:
Wesley Pattison
2022-10-09 19:08:39 +02:00
parent 89d1a0b9eb
commit 86cf4307c8
44 changed files with 513 additions and 185 deletions
@@ -0,0 +1,34 @@
/*
* Copyright 2022 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 { createApiRef } from '@backstage/core-plugin-api';
import {
AzureSiteListRequest,
AzureSiteListResponse,
AzureSiteStartStopRequest,
} from '@backstage/plugin-azure-sites-common';
/** @public */
export const azureSiteApiRef = createApiRef<AzureSitesApi>({
id: 'plugin.azure-sites.service',
});
/** @public */
export type AzureSitesApi = {
list: (request: AzureSiteListRequest) => Promise<AzureSiteListResponse>;
start: (request: AzureSiteStartStopRequest) => Promise<void>;
stop: (request: AzureSiteStartStopRequest) => Promise<void>;
};
@@ -0,0 +1,90 @@
/*
* Copyright 2022 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 { AzureSitesApi } from './AzureSitesApi';
import {
AzureSiteListRequest,
AzureSiteListResponse,
AzureSiteStartStopRequest,
} from '@backstage/plugin-azure-sites-common';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
/** @public */
export class AzureSitesApiBackendClient implements AzureSitesApi {
private readonly identityApi: IdentityApi;
private readonly discoveryApi: DiscoveryApi;
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
}
async stop(request: AzureSiteStartStopRequest): Promise<void> {
try {
const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/${
request.subscription
}/${request.resourceGroup}/${request.name}/stop`;
const { token: accessToken } = await this.identityApi.getCredentials();
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
});
} catch (e: any) {
throw new Error(e);
}
}
async start(request: AzureSiteStartStopRequest): Promise<void> {
try {
const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/${
request.subscription
}/${request.resourceGroup}/${request.name}/start`;
const { token: accessToken } = await this.identityApi.getCredentials();
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
});
} catch (e: any) {
throw new Error(e);
}
}
async list(request: AzureSiteListRequest): Promise<AzureSiteListResponse> {
try {
const url = `${await this.discoveryApi.getBaseUrl('azure-sites')}/list/${
request.name
}`;
const { token: accessToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
});
return await response.json();
} catch (e: any) {
throw new Error(e);
}
}
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2022 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 * from './AzureSitesApi';
export * from './AzureSitesApiBackendClient';
@@ -0,0 +1,76 @@
/*
* Copyright 2022 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 { useSites } from '../../hooks/useSites';
import {
AZURE_WEB_SITE_NAME_ANNOTATION,
useServiceEntityAnnotations,
} from '../../hooks/useServiceEntityAnnotations';
import {
ErrorBoundary,
MissingAnnotationEmptyState,
ResponseErrorPanel,
} from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import { AzureSitesOverviewTable } from '../AzureSitesOverviewTableComponent/AzureSitesOverviewTable';
/** @public */
export const isAzureWebSiteNameAvailable = (entity: Entity) =>
entity?.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION];
const AzureSitesOverview = ({ entity }: { entity: Entity }) => {
const { webSiteName } = useServiceEntityAnnotations(entity);
const [sites] = useSites({
name: webSiteName,
});
if (sites.error) {
return (
<div>
<ResponseErrorPanel error={sites.error} />
</div>
);
}
return (
<AzureSitesOverviewTable
data={sites.data?.items ?? []}
loading={sites.loading}
/>
);
};
/** @public */
export const AzureSitesOverviewWidget = () => {
const { entity } = useEntity();
if (!isAzureWebSiteNameAvailable(entity)) {
return (
<MissingAnnotationEmptyState
annotation={AZURE_WEB_SITE_NAME_ANNOTATION}
/>
);
}
return (
<ErrorBoundary>
<AzureSitesOverview entity={entity} />
</ErrorBoundary>
);
};
@@ -0,0 +1,86 @@
/*
* Copyright 2022 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 } from '@testing-library/react';
import {
AnyApiRef,
configApiRef,
errorApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { rest } from 'msw';
import {
setupRequestMockHandlers,
TestApiProvider,
} from '@backstage/test-utils';
import { setupServer } from 'msw/node';
import { DateTime } from 'luxon';
import { siteMock } from '../../mocks/mocks';
import { azureSiteApiRef } from '../..';
import { AzureSitesOverviewTable } from './AzureSitesOverviewTable';
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
const identityApiMock = (getCredentials: any) => ({
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials,
});
const azureSitesApiMock = {};
const config = {
getString: (_: string) => 'https://test-url',
};
const apis: [AnyApiRef, Partial<unknown>][] = [
[errorApiRef, errorApiMock],
[configApiRef, config],
[azureSiteApiRef, azureSitesApiMock],
[identityApiRef, identityApiMock],
];
describe('AzureSitesOverviewWidget', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
beforeEach(() => {
worker.use(
rest.get('/list/func-mock', (_, res, ctx) => {
res(ctx.json(siteMock));
}),
);
});
it('should display an overview table with the data from the requests', async () => {
const rendered = render(
<TestApiProvider apis={apis}>
<AzureSitesOverviewTable data={[siteMock]} loading={false} />
</TestApiProvider>,
);
expect(await rendered.findByText(siteMock.name)).toBeInTheDocument();
expect(await rendered.findByText(siteMock.location)).toBeInTheDocument();
expect(await rendered.findByText(siteMock.state)).toBeInTheDocument();
expect(
await rendered.findByText(
DateTime.fromISO(siteMock.lastModifiedDate).toLocaleString(
DateTime.DATETIME_MED,
),
),
).toBeInTheDocument();
});
});
@@ -0,0 +1,271 @@
/*
* Copyright 2022 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, { Dispatch, useEffect, useState } from 'react';
import {
Box,
Card,
IconButton,
LinearProgress,
Link,
Menu,
MenuItem,
Snackbar,
Tooltip,
} from '@material-ui/core';
import { default as MuiAlert } from '@material-ui/lab/Alert';
import { AzureSite } from '@backstage/plugin-azure-sites-common';
import { Table, TableColumn } from '@backstage/core-components';
import FlashOnIcon from '@material-ui/icons/FlashOn';
import PublicIcon from '@material-ui/icons/Public';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import StartIcon from '@material-ui/icons/PlayArrow';
import StopIcon from '@material-ui/icons/Stop';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import { DateTime } from 'luxon';
import { useApi } from '@backstage/core-plugin-api';
import { azureSiteApiRef } from '../../api';
type States = 'Waiting' | 'Running' | 'Paused' | 'Failed' | 'Stopped';
type Kinds = 'app' | 'functionapp';
const State = ({ value }: { value: States }) => {
const colorMap = {
Waiting: '#dcbc21',
Running: 'green',
Paused: 'black',
Failed: 'red',
Stopped: 'black',
};
return (
<Box display="flex" alignItems="center">
<span
style={{
display: 'block',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: colorMap[value],
marginRight: '5px',
}}
/>
{value}
</Box>
);
};
const Kind = ({ value }: { value: Kinds }) => {
const iconMap = {
app: <PublicIcon />,
functionapp: <FlashOnIcon />,
};
return (
<Box display="flex" alignItems="center">
<Tooltip title={value}>{iconMap[value]}</Tooltip>
</Box>
);
};
type TableProps = {
data: AzureSite[];
loading: boolean;
};
const ActionButtons = ({
value,
onMenuItemClick,
}: {
value: AzureSite;
onMenuItemClick: Dispatch<React.SetStateAction<string | null>>;
}) => {
const azureApi = useApi(azureSiteApiRef);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const start = () => {
azureApi.start({
name: value.name,
resourceGroup: value.resourceGroup,
subscription: value.subscription,
});
onMenuItemClick('Starting, this may take some time...');
handleClose();
};
const stop = () => {
azureApi.stop({
name: value.name,
resourceGroup: value.resourceGroup,
subscription: value.subscription,
});
onMenuItemClick('Stopping, this may take some time...');
handleClose();
};
return (
<div>
<IconButton
aria-label="more"
id="long-button"
aria-controls={open ? 'long-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
onClick={handleOpen}
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
MenuListProps={{
'aria-labelledby': 'long-button',
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
PaperProps={{
style: {
maxHeight: 48 * 4.5,
width: '20ch',
},
}}
>
{value.state !== 'Running' && (
<MenuItem key="start" onClick={start}>
<StartIcon />
&nbsp;Start
</MenuItem>
)}
{value.state !== 'Stopped' && (
<MenuItem key="stop" onClick={stop}>
<StopIcon />
&nbsp;Stop
</MenuItem>
)}
<MenuItem
component="a"
href={value.logstreamHref}
target="_blank"
key="logStream"
onClick={handleClose}
>
<OpenInNewIcon />
&nbsp;Log Stream
</MenuItem>
</Menu>
</div>
);
};
/** @public */
export const AzureSitesOverviewTable = ({ data, loading }: TableProps) => {
const [snackbarMessage, setSnackbarMessage] = useState<null | string>(null);
const [isSnackbarOpen, setSnackbarOpen] = useState(false);
const onSnackbarClose = () => {
setSnackbarMessage(null);
};
useEffect(() => {
setSnackbarOpen(!!snackbarMessage);
}, [snackbarMessage]);
const columns: TableColumn<AzureSite>[] = [
{
width: '25px',
field: 'kind',
render: (func: AzureSite) => <Kind value={func.kind as Kinds} />,
},
{
title: 'name',
field: 'name',
highlight: true,
render: (func: AzureSite) => {
return (
<Link href={func.href} target="_blank">
{func.name}
</Link>
);
},
},
{
title: 'location',
field: 'location',
render: (func: AzureSite) => func.location ?? 'unknown',
},
{
title: 'status',
field: 'status',
render: (func: AzureSite) => <State value={func.state as States} />,
},
{
title: 'last modified',
field: 'lastModifiedDate',
render: (func: AzureSite) =>
DateTime.fromISO(func.lastModifiedDate).toLocaleString(
DateTime.DATETIME_MED,
),
},
{
title: 'actions',
align: 'right',
sorting: false,
field: 'actions',
render: (func: AzureSite) => (
<ActionButtons value={func} onMenuItemClick={setSnackbarMessage} />
),
},
];
const tableStyle = {
minWidth: '0',
width: '100%',
};
return (
<Card style={tableStyle}>
<Table
title={
<Box display="flex" alignItems="center">
<FlashOnIcon style={{ fontSize: 30 }} />
<Box mr={1} />
Azure Sites
</Box>
}
options={{ paging: true, search: false, pageSize: 10 }}
data={data}
emptyContent={<LinearProgress />}
isLoading={loading}
columns={columns}
/>
<Snackbar
open={isSnackbarOpen}
autoHideDuration={6_000}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
onClose={onSnackbarClose}
>
<MuiAlert onClose={onSnackbarClose} severity="info">
{snackbarMessage}
</MuiAlert>
</Snackbar>
</Card>
);
};
@@ -0,0 +1,27 @@
/*
* Copyright 2022 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 AZURE_WEB_SITE_NAME_ANNOTATION = 'azure.com/microsoft-web-sites';
export const useServiceEntityAnnotations = (entity: Entity) => {
const webSiteName =
entity?.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION] ?? '';
return {
webSiteName,
};
};
+72
View File
@@ -0,0 +1,72 @@
/*
* Copyright 2022 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 useInterval from 'react-use/lib/useInterval';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { AzureSiteListResponse } from '@backstage/plugin-azure-sites-common';
import { azureSiteApiRef } from '../api';
import { useCallback } from 'react';
const POLLING_INTERVAL = 15000;
export function useSites({ name }: { name: string }) {
const azureApi = useApi(azureSiteApiRef);
const errorApi = useApi(errorApiRef);
const list = useCallback(
async () => {
return await azureApi.list({
name: name,
});
},
[name], // eslint-disable-line react-hooks/exhaustive-deps
);
const {
loading,
value: data,
error,
retry,
} = useAsyncRetry<AzureSiteListResponse | null>(async () => {
try {
const sites = await list();
return sites;
} catch (e) {
if (e instanceof Error) {
if (e?.message === 'AbortError') {
errorApi.post(
new Error(
'Timeout reaching backend plugin, please add azure-backend plugin',
),
);
}
errorApi.post(e);
}
return null;
}
}, []);
useInterval(() => retry(), POLLING_INTERVAL);
return [
{
loading,
data,
error,
retry,
},
] as const;
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2022 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 * from './plugin';
export * from './api';
export * from './components/AzureSitesOverviewComponent/AzureSitesOverview';
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright 2022 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 { AzureSite } from '@backstage/plugin-azure-sites-common';
export const entityMock = {
metadata: {
namespace: 'default',
annotations: {
'azure.com/microsoft-web-sites': 'func-mock',
},
name: 'sample-azure-service',
description: 'A service for testing Backstage functionality.',
uid: 'c009b513-d053-4b3f-9429-8433a145e943',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
spec: {
type: 'service',
owner: 'dev_group@example.com',
lifecycle: 'experimental',
},
};
// https://management.azure.com/subscriptions/{{subscriptionId}}/resourceGroups/{{resourceGroup}}/providers/Microsoft.Web/sites/{{functionsName}}?api-version=2022-03-01
export const siteMock: AzureSite = {
name: 'func-mock',
kind: 'functionapp',
resourceGroup: 'mock-resourcegroup',
subscription: '00000000-0000-0000-0000-000000000000',
tags: {
isMock: true,
},
location: 'West Europe',
state: 'Running',
href: 'https://mockurl.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/rg_mock-WestEuropewebspace/sites/func-mock',
logstreamHref:
'https://mockurl.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/rg_mock-WestEuropewebspace/sites/func-mock/logStream',
usageState: 'Normal',
lastModifiedDate: '2022-09-02T11:09:58.9033333',
containerSize: 100,
};
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2022 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 { azureSitesPlugin } from './plugin';
describe('azure', () => {
it('should export plugin', () => {
expect(azureSitesPlugin).toBeDefined();
});
});
+62
View File
@@ -0,0 +1,62 @@
/*
* Copyright 2022 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 {
createApiFactory,
createComponentExtension,
createPlugin,
createRouteRef,
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { azureSiteApiRef, AzureSitesApiBackendClient } from './api';
/** @public */
export const entityContentRouteRef = createRouteRef({
id: 'Azure Entity Content',
});
/** @public */
export const azureSitesPlugin = createPlugin({
id: 'azureFunctions',
apis: [
createApiFactory({
api: azureSiteApiRef,
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ discoveryApi, identityApi }) =>
new AzureSitesApiBackendClient({ discoveryApi, identityApi }),
}),
],
routes: {
entityContent: entityContentRouteRef,
},
});
/** @public */
export const EntityAzureSitesOverviewWidget = azureSitesPlugin.provide(
createComponentExtension({
name: 'EntityAzureSitesOverviewWidget',
component: {
lazy: () =>
import(
'./components/AzureSitesOverviewComponent/AzureSitesOverview'
).then(m => m.AzureSitesOverviewWidget),
},
}),
);
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2022 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 { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'azure-sites',
});
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2022 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 '@testing-library/jest-dom';
import 'cross-fetch/polyfill';