Merge branch 'master' of github.com:spotify/backstage into shmidt-i/circle-ci-plugin-new-route-api
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3",
|
||||
"swagger-ui-react": "^3.31.1"
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentEntity, Entity } from '@backstage/catalog-model';
|
||||
import { Progress } from '@backstage/core';
|
||||
import React, { FC } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import {
|
||||
ApiDefinitionCard,
|
||||
useComponentApiEntities,
|
||||
useComponentApiNames,
|
||||
} from '../../components';
|
||||
|
||||
export const EntityPageApi: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
const apiNames = useComponentApiNames(entity as ComponentEntity);
|
||||
|
||||
const { apiEntities, loading } = useComponentApiEntities({
|
||||
entity: entity as ComponentEntity,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
{apiNames.map(api => (
|
||||
<Grid item xs={12} key={api}>
|
||||
<ApiDefinitionCard title={api} apiEntity={apiEntities!.get(api)} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { EntityPageApi } from './EntityPageApi';
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { WarningPanel } from '@backstage/core';
|
||||
import { catalogRoute } from '../routes';
|
||||
import { EntityPageApi } from './EntityPageApi';
|
||||
|
||||
const isPluginApplicableToEntity = (entity: Entity) => {
|
||||
return ((entity.spec?.implementsApis as string[]) || []).length > 0;
|
||||
};
|
||||
|
||||
export const Router = ({ entity }: { entity: Entity }) =>
|
||||
// TODO(shmidt-i): move warning to a separate standardized component
|
||||
!isPluginApplicableToEntity(entity) ? (
|
||||
<WarningPanel title="API Docs plugin:">
|
||||
The entity doesn't implement any APIs.
|
||||
</WarningPanel>
|
||||
) : (
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/${catalogRoute.path}`}
|
||||
element={<EntityPageApi entity={entity} />}
|
||||
/>
|
||||
)
|
||||
</Routes>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Router } from './Router';
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core';
|
||||
// TODO: Circular ref!
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
*/
|
||||
|
||||
import { Content, useApi } from '@backstage/core';
|
||||
// TODO: Circular ref
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { ApiCatalogTable } from '../ApiCatalogTable/ApiCatalogTable';
|
||||
import { ApiCatalogTable } from '../ApiCatalogTable';
|
||||
import ApiCatalogLayout from './ApiCatalogLayout';
|
||||
|
||||
const CatalogPageContents = () => {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiCatalogPage } from './ApiCatalogPage';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiCatalogTable } from './ApiCatalogTable';
|
||||
@@ -14,23 +14,32 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { ApiEntity } from '@backstage/catalog-model';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import React from 'react';
|
||||
import { ApiDefinitionWidget } from '../ApiDefinitionWidget/ApiDefinitionWidget';
|
||||
import { ApiDefinitionWidget } from '../ApiDefinitionWidget';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
apiEntity: ApiEntityV1alpha1;
|
||||
apiEntity?: ApiEntity;
|
||||
};
|
||||
|
||||
export const ApiDefinitionCard = ({ title, apiEntity }: Props) => {
|
||||
const type = apiEntity?.spec?.type || '';
|
||||
const definition = apiEntity?.spec?.definition || '';
|
||||
if (!apiEntity) {
|
||||
return (
|
||||
<InfoCard title={title}>
|
||||
<Alert severity="error">Could not fetch the API</Alert>
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoCard title={title} subheader={type}>
|
||||
<ApiDefinitionWidget type={type} definition={definition} />
|
||||
<InfoCard title={title} subheader={apiEntity.spec.type}>
|
||||
<ApiDefinitionWidget
|
||||
type={apiEntity.spec.type}
|
||||
definition={apiEntity.spec.definition}
|
||||
/>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiDefinitionCard } from './ApiDefinitionCard';
|
||||
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget/AsyncApiDefinitionWidget';
|
||||
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget/OpenApiDefinitionWidget';
|
||||
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget/PlainApiDefinitionWidget';
|
||||
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
|
||||
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
|
||||
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget';
|
||||
|
||||
type Props = {
|
||||
type: string;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiDefinitionWidget } from './ApiDefinitionWidget';
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model';
|
||||
import { ApiEntity, Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Content,
|
||||
errorApiRef,
|
||||
@@ -25,14 +25,13 @@ import {
|
||||
Progress,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
// TODO: Circular ref
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { ApiDefinitionCard } from '../ApiDefinitionCard/ApiDefinitionCard';
|
||||
import { ApiDefinitionCard } from '../ApiDefinitionCard';
|
||||
|
||||
const REDIRECT_DELAY = 1000;
|
||||
function headerProps(
|
||||
@@ -125,7 +124,7 @@ export const ApiEntityPage = () => {
|
||||
{entity && (
|
||||
<>
|
||||
<Content>
|
||||
<ApiDefinitionCard apiEntity={entity as ApiEntityV1alpha1} />
|
||||
<ApiDefinitionCard apiEntity={entity as ApiEntity} />
|
||||
</Content>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiEntityPage } from './ApiEntityPage';
|
||||
+2
-2
@@ -48,7 +48,7 @@ const useStyles = makeStyles(theme => ({
|
||||
border: `1px solid ${fade(theme.palette.primary.main, 0.5)}`,
|
||||
'&:hover': {
|
||||
textDecoration: 'none',
|
||||
'&$disabled': {
|
||||
'&.Mui-disabled': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
@@ -61,7 +61,7 @@ const useStyles = makeStyles(theme => ({
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
},
|
||||
'&$disabled': {
|
||||
'&.Mui-disabled': {
|
||||
color: theme.palette.action.disabled,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget';
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiDefinitionCard } from './ApiDefinitionCard';
|
||||
export { useComponentApiNames } from './useComponentApiNames';
|
||||
export { useComponentApiEntities } from './useComponentApiEntities';
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { errorApiRef, useApi } from '@backstage/core';
|
||||
import { ApiEntity, ComponentEntity } from '@backstage/catalog-model';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { useComponentApiNames } from './useComponentApiNames';
|
||||
|
||||
export function useComponentApiEntities({
|
||||
entity,
|
||||
}: {
|
||||
entity: ComponentEntity;
|
||||
}): {
|
||||
loading: boolean;
|
||||
apiEntities?: Map<String, ApiEntity>;
|
||||
error?: Error;
|
||||
retry: () => void;
|
||||
} {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const apiNames = useComponentApiNames(entity);
|
||||
|
||||
const { loading, value: apiEntities, retry, error } = useAsyncRetry<
|
||||
Map<string, ApiEntity>
|
||||
>(async () => {
|
||||
const resultMap = new Map<string, ApiEntity>();
|
||||
|
||||
await Promise.all(
|
||||
apiNames.map(async name => {
|
||||
try {
|
||||
const api = (await catalogApi.getEntityByName({
|
||||
kind: 'API',
|
||||
name,
|
||||
})) as ApiEntity | undefined;
|
||||
|
||||
if (api) {
|
||||
resultMap.set(api.metadata.name, api);
|
||||
}
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return resultMap;
|
||||
}, [catalogApi, entity]);
|
||||
|
||||
return {
|
||||
apiEntities,
|
||||
loading,
|
||||
error,
|
||||
retry,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentEntity } from '@backstage/catalog-model';
|
||||
|
||||
export const useComponentApiNames = (entity: ComponentEntity) => {
|
||||
return (entity.spec?.implementsApis as string[]) || [];
|
||||
};
|
||||
@@ -14,5 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiDefinitionCard } from './components/ApiDefinitionCard/ApiDefinitionCard';
|
||||
export { Router } from './catalog';
|
||||
export { plugin } from './plugin';
|
||||
|
||||
@@ -28,3 +28,8 @@ export const entityRoute = createRouteRef({
|
||||
path: '/api-docs/:optionalNamespaceAndName/',
|
||||
title: 'API',
|
||||
});
|
||||
export const catalogRoute = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '',
|
||||
title: 'API',
|
||||
});
|
||||
|
||||
@@ -47,21 +47,21 @@ describe('createRouter', () => {
|
||||
const response = await request(app).get('/index.html');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toBe('this is index.html\n');
|
||||
expect(response.text.trim()).toBe('this is index.html');
|
||||
});
|
||||
|
||||
it('returns other.html', async () => {
|
||||
const response = await request(app).get('/other.html');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toBe('this is other.html\n');
|
||||
expect(response.text.trim()).toBe('this is other.html');
|
||||
});
|
||||
|
||||
it('returns index.html if missing', async () => {
|
||||
const response = await request(app).get('/missing.html');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toBe('this is index.html\n');
|
||||
expect(response.text.trim()).toBe('this is index.html');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,11 +83,11 @@ describe('createRouter with static fallback handler', () => {
|
||||
|
||||
const response1 = await request(app).get('/static/main.txt');
|
||||
expect(response1.status).toBe(200);
|
||||
expect(response1.text).toBe('this is main.txt\n');
|
||||
expect(response1.text.trim()).toBe('this is main.txt');
|
||||
|
||||
const response2 = await request(app).get('/static/test.txt');
|
||||
expect(response2.status).toBe(200);
|
||||
expect(response2.text).toBe('this is test.txt');
|
||||
expect(response2.text.trim()).toBe('this is test.txt');
|
||||
|
||||
const response3 = await request(app).get('/static/missing.txt');
|
||||
expect(response3.status).toBe(404);
|
||||
|
||||
@@ -85,8 +85,8 @@ Click [here](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMe
|
||||
- Give the app a name. e.g. `backstage-dev`
|
||||
- Select `Accounts in this organizational directory only` under supported account types.
|
||||
- Enter the callback URL for your backstage backend instance:
|
||||
- For local development, this is likely `http://localhost:7000/auth/microsoft/handler/frame`
|
||||
- For non-local deployments, this will be `https://{APP_FQDN}:{APP_BACKEND_PORT}/auth/microsoft/handler/frame`
|
||||
- For local development, this is likely `http://localhost:7000/auth/microsoft/handler/frame`
|
||||
- For non-local deployments, this will be `https://{APP_FQDN}:{APP_BACKEND_PORT}/auth/microsoft/handler/frame`
|
||||
- Click `Register`.
|
||||
|
||||
We also need to generate a client secret so Backstage can authenticate as this app.
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
} from '../../providers/types';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { verifyNonce, encodeState } from './helpers';
|
||||
import { verifyNonce } from './helpers';
|
||||
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
|
||||
import { OAuthHandlers } from './types';
|
||||
import { OAuthHandlers, OAuthStartRequest, OAuthRefreshRequest } from './types';
|
||||
|
||||
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
|
||||
export const TEN_MINUTES_MS = 600 * 1000;
|
||||
@@ -86,15 +86,12 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
// set a nonce cookie before redirecting to oauth provider
|
||||
this.setNonceCookie(res, nonce);
|
||||
|
||||
const stateObject = { nonce: nonce, env: env };
|
||||
const stateParameter = encodeState(stateObject);
|
||||
const state = { nonce: nonce, env: env };
|
||||
const forwardReq = Object.assign(req, { scope, state });
|
||||
|
||||
const queryParameters = {
|
||||
scope,
|
||||
state: stateParameter,
|
||||
};
|
||||
|
||||
const { url, status } = await this.handlers.start(req, queryParameters);
|
||||
const { url, status } = await this.handlers.start(
|
||||
forwardReq as OAuthStartRequest,
|
||||
);
|
||||
|
||||
res.statusCode = status || 302;
|
||||
res.setHeader('Location', url);
|
||||
@@ -185,8 +182,12 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
|
||||
const scope = req.query.scope?.toString() ?? '';
|
||||
|
||||
const forwardReq = Object.assign(req, { scope, refreshToken });
|
||||
|
||||
// get new access_token
|
||||
const response = await this.handlers.refresh(refreshToken, scope);
|
||||
const response = await this.handlers.refresh(
|
||||
forwardReq as OAuthRefreshRequest,
|
||||
);
|
||||
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
|
||||
export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler';
|
||||
export { OAuthAdapter } from './OAuthAdapter';
|
||||
export { encodeState } from './helpers';
|
||||
export type {
|
||||
OAuthHandlers,
|
||||
OAuthProviderInfo,
|
||||
OAuthProviderOptions,
|
||||
OAuthResponse,
|
||||
OAuthState,
|
||||
OAuthStartRequest,
|
||||
OAuthRefreshRequest,
|
||||
} from './types';
|
||||
|
||||
@@ -67,6 +67,16 @@ export type OAuthState = {
|
||||
env: string;
|
||||
};
|
||||
|
||||
export type OAuthStartRequest = express.Request<{}> & {
|
||||
scope: string;
|
||||
state: OAuthState;
|
||||
};
|
||||
|
||||
export type OAuthRefreshRequest = express.Request<{}> & {
|
||||
scope: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Any OAuth provider needs to implement this interface which has provider specific
|
||||
* handlers for different methods to perform authentication, get access tokens,
|
||||
@@ -78,10 +88,7 @@ export interface OAuthHandlers {
|
||||
* @param {express.Request} req
|
||||
* @param options
|
||||
*/
|
||||
start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo>;
|
||||
start(req: OAuthStartRequest): Promise<RedirectInfo>;
|
||||
|
||||
/**
|
||||
* Handles the redirect from the auth provider when the user has signed in.
|
||||
@@ -99,10 +106,7 @@ export interface OAuthHandlers {
|
||||
* @param {string} refreshToken
|
||||
* @param {string} scope
|
||||
*/
|
||||
refresh?(
|
||||
refreshToken: string,
|
||||
scope: string,
|
||||
): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
refresh?(req: OAuthRefreshRequest): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
|
||||
/**
|
||||
* (Optional) Sign out of the auth provider.
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
OAuthRefreshRequest,
|
||||
} from '../../lib/oauth';
|
||||
import {
|
||||
executeFetchUserProfileStrategy,
|
||||
@@ -81,16 +84,13 @@ export class Auth0AuthProvider implements OAuthHandlers {
|
||||
);
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo> {
|
||||
const providerOptions = {
|
||||
...options,
|
||||
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
};
|
||||
return await executeRedirectStrategy(req, this._strategy, providerOptions);
|
||||
scope: req.scope,
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(
|
||||
@@ -107,11 +107,11 @@ export class Auth0AuthProvider implements OAuthHandlers {
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
|
||||
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
|
||||
const { accessToken, params } = await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
refreshToken,
|
||||
scope,
|
||||
req.refreshToken,
|
||||
req.scope,
|
||||
);
|
||||
|
||||
const profile = await executeFetchUserProfileStrategy(
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
|
||||
@@ -117,11 +119,11 @@ export class GithubAuthProvider implements OAuthHandlers {
|
||||
);
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, options);
|
||||
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
scope: req.scope,
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(req: express.Request) {
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
|
||||
@@ -122,11 +124,11 @@ export class GitlabAuthProvider implements OAuthHandlers {
|
||||
);
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, options);
|
||||
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
scope: req.scope,
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(req: express.Request): Promise<{ response: OAuthResponse }> {
|
||||
|
||||
@@ -31,6 +31,9 @@ import {
|
||||
OAuthProviderOptions,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
OAuthRefreshRequest,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
|
||||
@@ -79,16 +82,13 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
);
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo> {
|
||||
const providerOptions = {
|
||||
...options,
|
||||
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
};
|
||||
return await executeRedirectStrategy(req, this._strategy, providerOptions);
|
||||
scope: req.scope,
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(
|
||||
@@ -105,11 +105,11 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
|
||||
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
|
||||
const { accessToken, params } = await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
refreshToken,
|
||||
scope,
|
||||
req.refreshToken,
|
||||
req.scope,
|
||||
);
|
||||
|
||||
const profile = await executeFetchUserProfileStrategy(
|
||||
|
||||
@@ -35,6 +35,9 @@ import {
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
OAuthRefreshRequest,
|
||||
} from '../../lib/oauth';
|
||||
|
||||
import got from 'got';
|
||||
@@ -111,11 +114,11 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
|
||||
);
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, options);
|
||||
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
scope: req.scope,
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(
|
||||
@@ -132,11 +135,11 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
|
||||
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
|
||||
const { accessToken, params } = await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
refreshToken,
|
||||
scope,
|
||||
req.refreshToken,
|
||||
req.scope,
|
||||
);
|
||||
|
||||
const profile = await executeFetchUserProfileStrategy(
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
OAuthRefreshRequest,
|
||||
} from '../../lib/oauth';
|
||||
import {
|
||||
executeFetchUserProfileStrategy,
|
||||
@@ -84,16 +87,13 @@ export class OAuth2AuthProvider implements OAuthHandlers {
|
||||
);
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo> {
|
||||
const providerOptions = {
|
||||
...options,
|
||||
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
};
|
||||
return await executeRedirectStrategy(req, this._strategy, providerOptions);
|
||||
scope: req.scope,
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(
|
||||
@@ -110,11 +110,11 @@ export class OAuth2AuthProvider implements OAuthHandlers {
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
|
||||
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
|
||||
const refreshTokenResponse = await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
refreshToken,
|
||||
scope,
|
||||
req.refreshToken,
|
||||
req.scope,
|
||||
);
|
||||
const {
|
||||
accessToken,
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { createOktaProvider } from './provider';
|
||||
export { createOktaProvider } from './provider';
|
||||
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
OAuthRefreshRequest,
|
||||
} from '../../lib/oauth';
|
||||
import { Strategy as OktaStrategy } from 'passport-okta-oauth';
|
||||
import passport from 'passport';
|
||||
@@ -101,16 +104,13 @@ export class OktaAuthProvider implements OAuthHandlers {
|
||||
);
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo> {
|
||||
const providerOptions = {
|
||||
...options,
|
||||
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
};
|
||||
return await executeRedirectStrategy(req, this._strategy, providerOptions);
|
||||
scope: req.scope,
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(
|
||||
@@ -127,11 +127,11 @@ export class OktaAuthProvider implements OAuthHandlers {
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
|
||||
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
|
||||
const { accessToken, params } = await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
refreshToken,
|
||||
scope,
|
||||
req.refreshToken,
|
||||
req.scope,
|
||||
);
|
||||
|
||||
const profile = await executeFetchUserProfileStrategy(
|
||||
|
||||
+1
-3
@@ -14,9 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
declare module 'passport-okta-oauth' {
|
||||
|
||||
export class Strategy {
|
||||
constructor(options: any, verify: any)
|
||||
constructor(options: any, verify: any);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-api-docs": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-github-actions": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-jenkins": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.21",
|
||||
|
||||
@@ -104,7 +104,9 @@ export function AboutCard({ entity }: AboutCardProps) {
|
||||
<IconLinkVertical
|
||||
label="View Techdocs"
|
||||
icon={<DocsIcon />}
|
||||
href={`/docs/${''}`}
|
||||
href={`/docs/${entity.kind}:${entity.metadata.namespace ?? ''}:${
|
||||
entity.metadata.name
|
||||
}`}
|
||||
/>
|
||||
</nav>
|
||||
}
|
||||
|
||||
@@ -31,12 +31,13 @@ type Props = {
|
||||
|
||||
const CatalogLayout = ({ children }: Props) => {
|
||||
const greeting = getTimeBasedGreeting();
|
||||
const profile = useApi(identityApiRef).getProfile();
|
||||
const userId = useApi(identityApiRef).getUserId();
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header
|
||||
title={`${greeting.greeting}, ${userId}!`}
|
||||
title={`${greeting.greeting}, ${profile.displayName || userId}!`}
|
||||
subtitle="Backstage Service Catalog"
|
||||
tooltip={greeting.language}
|
||||
pageTitleOverride="Home"
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
IdentityApi,
|
||||
identityApiRef,
|
||||
storageApiRef,
|
||||
ProfileInfo,
|
||||
} from '@backstage/core';
|
||||
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
@@ -60,8 +61,12 @@ describe('CatalogPage', () => {
|
||||
getLocationByEntity: () =>
|
||||
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
|
||||
};
|
||||
const testProfile: Partial<ProfileInfo> = {
|
||||
displayName: 'Display Name',
|
||||
};
|
||||
const indentityApi: Partial<IdentityApi> = {
|
||||
getUserId: () => 'tools@example.com',
|
||||
getProfile: () => testProfile,
|
||||
};
|
||||
|
||||
const renderWrapped = (children: React.ReactNode) =>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model';
|
||||
import { Content, Progress, useApi } from '@backstage/core';
|
||||
import { ApiDefinitionCard } from '@backstage/plugin-api-docs';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { FC } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../..';
|
||||
|
||||
export const EntityPageApi: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const { value: apiEntities, loading } = useAsync(async () => {
|
||||
const a = await Promise.all(
|
||||
((entity?.spec?.implementsApis as string[]) || []).map(api =>
|
||||
catalogApi.getEntityByName({
|
||||
kind: 'API',
|
||||
name: api,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const b = new Map<string, ApiEntityV1alpha1>();
|
||||
|
||||
a.filter(api => !!api).forEach(api => {
|
||||
b.set(api?.metadata?.name!, api as ApiEntityV1alpha1);
|
||||
});
|
||||
return b;
|
||||
}, [catalogApi, entity]);
|
||||
|
||||
return (
|
||||
<Content>
|
||||
{loading && <Progress />}
|
||||
{!loading && (
|
||||
<Grid container spacing={3}>
|
||||
{((entity?.spec?.implementsApis as string[]) || []).map(api => {
|
||||
const apiEntity = apiEntities && apiEntities.get(api);
|
||||
|
||||
return (
|
||||
<Grid item sm={12} key={api}>
|
||||
{!apiEntity && (
|
||||
<Alert severity="error">
|
||||
Error on fetching the API: {api}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{apiEntity && (
|
||||
<ApiDefinitionCard title={api} apiEntity={apiEntity} />
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
)}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,10 @@
|
||||
// TODO(shmidt-i): move to the app
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Content } from '@backstage/core';
|
||||
import { LatestWorkflowsForBranchCard } from '@backstage/plugin-github-actions';
|
||||
import {
|
||||
LatestWorkflowsForBranchCard,
|
||||
GITHUB_ACTIONS_ANNOTATION,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
@@ -25,7 +28,7 @@ export const EntityPageCi: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
return (
|
||||
<Content>
|
||||
<Grid container spacing={3}>
|
||||
{entity.metadata?.annotations?.['backstage.io/github-actions-id'] && (
|
||||
{entity.metadata?.annotations?.[GITHUB_ACTIONS_ANNOTATION] && (
|
||||
<Grid item sm={12}>
|
||||
<LatestWorkflowsForBranchCard entity={entity} branch="master" />
|
||||
</Grid>
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
// TODO(shmidt-i): move to the app
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Content } from '@backstage/core';
|
||||
import { LatestWorkflowRunCard } from '@backstage/plugin-github-actions';
|
||||
import {
|
||||
LatestWorkflowRunCard,
|
||||
GITHUB_ACTIONS_ANNOTATION,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
import {
|
||||
JenkinsBuildsWidget,
|
||||
JenkinsLastBuildWidget,
|
||||
@@ -47,7 +50,7 @@ export const EntityPageOverview: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
<JenkinsBuildsWidget entity={entity} />
|
||||
</Grid>
|
||||
)}
|
||||
{entity.metadata?.annotations?.['backstage.io/github-actions-id'] && (
|
||||
{entity.metadata?.annotations?.[GITHUB_ACTIONS_ANNOTATION] && (
|
||||
<Grid item sm={3}>
|
||||
<LatestWorkflowRunCard entity={entity} branch="master" />
|
||||
</Grid>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Title
|
||||
|
||||
Welcome to the explore plugin!
|
||||
|
||||
## Sub-section 1
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
@@ -17,6 +17,4 @@
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { plugin } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(plugin)
|
||||
.render();
|
||||
createDevApp().registerPlugin(plugin).render();
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import ExternalLinkIcon from '@material-ui/icons/Launch';
|
||||
import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName';
|
||||
|
||||
const useStyles = makeStyles<Theme>({
|
||||
externalLinkIcon: {
|
||||
@@ -83,7 +84,7 @@ export const LatestWorkflowRunCard = ({
|
||||
}) => {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const [owner, repo] = (
|
||||
entity?.metadata.annotations?.['backstage.io/github-actions-id'] ?? '/'
|
||||
entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/'
|
||||
).split('/');
|
||||
const [{ runs, loading, error }] = useWorkflowRuns({
|
||||
owner,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const GraphiQL = React.lazy(() => import('graphiql'));
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
root: {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
|
||||
@@ -31,7 +31,7 @@ const columns: TableColumn[] = [
|
||||
title: 'Website URL',
|
||||
field: 'websiteUrl',
|
||||
},
|
||||
...CATEGORIES.map((category) => ({
|
||||
...CATEGORIES.map(category => ({
|
||||
title: CATEGORY_LABELS[category],
|
||||
field: category,
|
||||
})),
|
||||
@@ -56,7 +56,7 @@ export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => {
|
||||
const lighthouseApi = useApi(lighthouseApiRef);
|
||||
|
||||
const runRefresh = (websites: Website[]) => {
|
||||
websites.forEach(async (website) => {
|
||||
websites.forEach(async website => {
|
||||
const response = await lighthouseApi.getWebsiteForAuditId(
|
||||
website.lastAudit.id,
|
||||
);
|
||||
@@ -64,7 +64,7 @@ export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => {
|
||||
if (auditStatus === 'COMPLETED' || auditStatus === 'FAILED') {
|
||||
const newWebsiteData = websiteState.slice(0);
|
||||
newWebsiteData[
|
||||
newWebsiteData.findIndex((w) => w.url === response.url)
|
||||
newWebsiteData.findIndex(w => w.url === response.url)
|
||||
] = response;
|
||||
setWebsiteState(newWebsiteData);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => {
|
||||
};
|
||||
|
||||
const runningWebsiteAudits = websiteState
|
||||
? websiteState.filter((website) => website.lastAudit.status === 'RUNNING')
|
||||
? websiteState.filter(website => website.lastAudit.status === 'RUNNING')
|
||||
: [];
|
||||
|
||||
useInterval(
|
||||
@@ -80,10 +80,10 @@ export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => {
|
||||
runningWebsiteAudits.length > 0 ? 5000 : null,
|
||||
);
|
||||
|
||||
const data = websiteState.map((website) => {
|
||||
const data = websiteState.map(website => {
|
||||
const trendlineData = buildSparklinesDataForItem(website);
|
||||
const trendlines: any = {};
|
||||
CATEGORIES.forEach((category) => {
|
||||
CATEGORIES.forEach(category => {
|
||||
trendlines[category] = (
|
||||
<TrendLine
|
||||
title={`trendline for ${CATEGORY_LABELS[category]} category of ${website.url}`}
|
||||
|
||||
@@ -71,7 +71,7 @@ export default builder.build() as ApiHolder;
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
const useStyles = makeStyles(theme => ({
|
||||
tabs: { marginBottom: -18 },
|
||||
tab: { minWidth: 72, paddingLeft: 1, paddingRight: 1 },
|
||||
content: { marginBottom: theme.spacing(2) },
|
||||
|
||||
+46
-14
@@ -26,25 +26,57 @@ export { plugin as Rollbar } from '@backstage/plugin-rollbar';
|
||||
import { RollbarClient, rollbarApiRef } from '@backstage/plugin-rollbar';
|
||||
|
||||
// ...
|
||||
|
||||
builder.add(
|
||||
rollbarApiRef,
|
||||
new RollbarClient({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/rollbar',
|
||||
}),
|
||||
);
|
||||
|
||||
// Alternatively you can use the mock client
|
||||
// builder.add(rollbarApiRef, new RollbarMockClient());
|
||||
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
|
||||
```
|
||||
|
||||
5. Run app with `yarn start` and navigate to `/rollbar`
|
||||
5. Add to the app `EntityPage` component:
|
||||
|
||||
```ts
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
import { Router as RollbarRouter } from '@backstage/plugin-rollbar';
|
||||
|
||||
// ...
|
||||
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
// ...
|
||||
<EntityPageLayout.Content
|
||||
path="/rollbar"
|
||||
title="Errors"
|
||||
element={<RollbarRouter entity={entity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
```
|
||||
|
||||
6. Setup the `app.config.yaml` and account token environment variable
|
||||
|
||||
```yaml
|
||||
# app.config.yaml
|
||||
rollbar:
|
||||
organization: spotify
|
||||
accountToken:
|
||||
$secret:
|
||||
env: ROLLBAR_ACCOUNT_TOKEN
|
||||
```
|
||||
|
||||
7. Annotate entities with the rollbar project slug
|
||||
|
||||
```yaml
|
||||
# pump-station-catalog-component.yaml
|
||||
# ...
|
||||
metadata:
|
||||
annotations:
|
||||
rollbar.com/project-slug: organization-name/project-name
|
||||
# -- or just ---
|
||||
rollbar.com/project-slug: project-name
|
||||
```
|
||||
|
||||
8. Run app with `yarn start` and navigate to `/rollbar` or a catalog entity
|
||||
|
||||
## Features
|
||||
|
||||
- List rollbar projects
|
||||
- View top active items for each project
|
||||
- List rollbar entities that are annotated with `rollbar.com/project-slug`
|
||||
- View top active items for each rollbar annotated entity
|
||||
|
||||
## Limitations
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.21",
|
||||
"@backstage/core": "^0.1.1-alpha.21",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
|
||||
"@backstage/theme": "^0.1.1-alpha.21",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -29,6 +29,7 @@ export const rollbarApiRef = createApiRef<RollbarApi>({
|
||||
|
||||
export interface RollbarApi {
|
||||
getAllProjects(): Promise<RollbarProject[]>;
|
||||
getProject(projectName: string): Promise<RollbarProject>;
|
||||
getTopActiveItems(
|
||||
project: string,
|
||||
hours?: number,
|
||||
|
||||
@@ -30,9 +30,11 @@ export class RollbarClient implements RollbarApi {
|
||||
}
|
||||
|
||||
async getAllProjects(): Promise<RollbarProject[]> {
|
||||
const path = `/projects`;
|
||||
return await this.get(`/projects`);
|
||||
}
|
||||
|
||||
return await this.get(path);
|
||||
async getProject(projectName: string): Promise<RollbarProject> {
|
||||
return await this.get(`/projects/${projectName}`);
|
||||
}
|
||||
|
||||
async getTopActiveItems(
|
||||
@@ -40,15 +42,13 @@ export class RollbarClient implements RollbarApi {
|
||||
hours = 24,
|
||||
environment = 'production',
|
||||
): Promise<RollbarTopActiveItem[]> {
|
||||
const path = `/projects/${project}/top_active_items?environment=${environment}&hours=${hours}`;
|
||||
|
||||
return await this.get(path);
|
||||
return await this.get(
|
||||
`/projects/${project}/top_active_items?environment=${environment}&hours=${hours}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getProjectItems(project: string): Promise<RollbarItemsResponse> {
|
||||
const path = `/projects/${project}/items`;
|
||||
|
||||
return await this.get(path);
|
||||
return await this.get(`/projects/${project}/items`);
|
||||
}
|
||||
|
||||
private async get(path: string): Promise<any> {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { RollbarApi } from './RollbarApi';
|
||||
import {
|
||||
RollbarItemsResponse,
|
||||
RollbarProject,
|
||||
RollbarTopActiveItem,
|
||||
} from './types';
|
||||
|
||||
export class RollbarMockClient implements RollbarApi {
|
||||
async getAllProjects(): Promise<RollbarProject[]> {
|
||||
return Promise.resolve([
|
||||
{ id: 123, name: 'project-a', accountId: 1, status: 'enabled' },
|
||||
{ id: 356, name: 'project-b', accountId: 1, status: 'enabled' },
|
||||
{ id: 789, name: 'project-c', accountId: 1, status: 'enabled' },
|
||||
]);
|
||||
}
|
||||
|
||||
async getTopActiveItems(
|
||||
_project: string,
|
||||
_hours = 24,
|
||||
_environment = 'production',
|
||||
): Promise<RollbarTopActiveItem[]> {
|
||||
const createItem = (id: number): RollbarTopActiveItem => ({
|
||||
item: {
|
||||
id,
|
||||
counter: id,
|
||||
environment: 'production',
|
||||
framework: 2,
|
||||
lastOccurrenceTimestamp: new Date().getTime() / 1000,
|
||||
level: 50,
|
||||
occurrences: 100,
|
||||
projectId: 12345,
|
||||
title: `Some error occurred in service - ${id}`,
|
||||
uniqueOccurrences: 10,
|
||||
},
|
||||
counts: Array.from({ length: 168 }, () =>
|
||||
Math.floor(Math.random() * 100),
|
||||
),
|
||||
});
|
||||
|
||||
const items = Array.from({ length: 10 }, (_, i) => createItem(i));
|
||||
|
||||
return Promise.resolve(items);
|
||||
}
|
||||
|
||||
async getProjectItems(_project: string): Promise<RollbarItemsResponse> {
|
||||
return Promise.resolve({
|
||||
items: [],
|
||||
page: 0,
|
||||
totalCount: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './RollbarApi';
|
||||
export * from './RollbarClient';
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { RollbarProject } from '../RollbarProject/RollbarProject';
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
export const EntityPageRollbar = ({ entity }: Props) => {
|
||||
return <RollbarProject entity={entity} />;
|
||||
};
|
||||
+12
-3
@@ -21,13 +21,14 @@ import {
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi';
|
||||
import { RollbarProject } from '../../api/types';
|
||||
import { RollbarPage } from './RollbarPage';
|
||||
import { RollbarHome } from './RollbarHome';
|
||||
|
||||
describe('RollbarPage component', () => {
|
||||
describe('RollbarHome component', () => {
|
||||
const projects: RollbarProject[] = [
|
||||
{ id: 123, name: 'abc', accountId: 1, status: 'enabled' },
|
||||
{ id: 456, name: 'xyz', accountId: 1, status: 'enabled' },
|
||||
@@ -47,6 +48,14 @@ describe('RollbarPage component', () => {
|
||||
apis={ApiRegistry.from([
|
||||
[rollbarApiRef, rollbarApi],
|
||||
[configApiRef, config],
|
||||
[
|
||||
catalogApiRef,
|
||||
({
|
||||
async getEntities() {
|
||||
return [];
|
||||
},
|
||||
} as Partial<CatalogApi>) as CatalogApi,
|
||||
],
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
@@ -55,7 +64,7 @@ describe('RollbarPage component', () => {
|
||||
);
|
||||
|
||||
it('should render rollbar landing page', async () => {
|
||||
const rendered = renderWrapped(<RollbarPage />);
|
||||
const rendered = renderWrapped(<RollbarHome />);
|
||||
expect(rendered.getByText(/Rollbar/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+12
-28
@@ -14,42 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import {
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Page,
|
||||
pageTheme,
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
} from '@backstage/core';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { Content, Header, Page, pageTheme } from '@backstage/core';
|
||||
import { RollbarProjectTable } from '../RollbarProjectTable/RollbarProjectTable';
|
||||
import { useRollbarEntities } from '../../hooks/useRollbarEntities';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
export const RollbarHome = () => {
|
||||
const { entities, loading, error } = useRollbarEntities();
|
||||
|
||||
export const RollbarLayout = ({ title = 'Dashboard', children }: Props) => {
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Rollbar"
|
||||
subtitle="Real-time error tracking & debugging tools for developers"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
/>
|
||||
<Content>
|
||||
<ContentHeader title={title}>
|
||||
<SupportButton>
|
||||
Rollbar plugin allows you to preview issues and navigate to rollbar.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
{children}
|
||||
</Grid>
|
||||
<RollbarProjectTable
|
||||
entities={entities || []}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useTopActiveItems } from '../../hooks/useTopActiveItems';
|
||||
import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable';
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
export const RollbarProject = ({ entity }: Props) => {
|
||||
const { items, organization, project, loading, error } = useTopActiveItems(
|
||||
entity,
|
||||
);
|
||||
|
||||
return (
|
||||
<RollbarTopItemsTable
|
||||
organization={organization}
|
||||
project={project}
|
||||
items={items || []}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import { render } from '@testing-library/react';
|
||||
import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi';
|
||||
import { RollbarTopActiveItem } from '../../api/types';
|
||||
import { RollbarProjectPage } from './RollbarProjectPage';
|
||||
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog';
|
||||
|
||||
describe('RollbarProjectPage component', () => {
|
||||
const items: RollbarTopActiveItem[] = [
|
||||
@@ -60,6 +61,17 @@ describe('RollbarProjectPage component', () => {
|
||||
apis={ApiRegistry.from([
|
||||
[rollbarApiRef, rollbarApi],
|
||||
[configApiRef, config],
|
||||
[
|
||||
catalogApiRef,
|
||||
({
|
||||
async getEntityByName() {
|
||||
return {
|
||||
metadata: { name: 'foo' },
|
||||
spec: { owner: 'bar', lifecycle: 'experimental' },
|
||||
} as any;
|
||||
},
|
||||
} as Partial<CatalogApi>) as CatalogApi,
|
||||
],
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
@@ -69,6 +81,6 @@ describe('RollbarProjectPage component', () => {
|
||||
|
||||
it('should render rollbar project page', async () => {
|
||||
const rendered = renderWrapped(<RollbarProjectPage />);
|
||||
expect(rendered.getByText(/Top Active Items/)).toBeInTheDocument();
|
||||
expect(rendered.getByText(/Rollbar/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,39 +15,22 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { configApiRef, useApi } from '@backstage/core';
|
||||
import { rollbarApiRef } from '../../api/RollbarApi';
|
||||
import { RollbarLayout } from '../RollbarLayout/RollbarLayout';
|
||||
import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable';
|
||||
import { Content, Header, HeaderLabel, Page, pageTheme } from '@backstage/core';
|
||||
import { useCatalogEntity } from '../../hooks/useCatalogEntity';
|
||||
import { RollbarProject } from '../RollbarProject/RollbarProject';
|
||||
|
||||
export const RollbarProjectPage = () => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const rollbarApi = useApi(rollbarApiRef);
|
||||
const org =
|
||||
configApi.getOptionalString('rollbar.organization') ??
|
||||
configApi.getString('organization.name');
|
||||
const { componentId } = useParams() as {
|
||||
componentId: string;
|
||||
};
|
||||
const { value, loading, error } = useAsync(() =>
|
||||
rollbarApi
|
||||
.getTopActiveItems(componentId, 168)
|
||||
.then(data =>
|
||||
data.sort((a, b) => b.item.occurrences - a.item.occurrences),
|
||||
),
|
||||
);
|
||||
const { entity } = useCatalogEntity();
|
||||
|
||||
return (
|
||||
<RollbarLayout>
|
||||
<RollbarTopItemsTable
|
||||
items={value || []}
|
||||
organization={org}
|
||||
project={componentId}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
</RollbarLayout>
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title={entity?.metadata?.name} subtitle="Rollbar Project">
|
||||
<HeaderLabel label="Owner" value={entity?.spec?.owner} />
|
||||
<HeaderLabel label="Lifecycle" value={entity?.spec?.lifecycle} />
|
||||
</Header>
|
||||
<Content>
|
||||
{entity ? <RollbarProject entity={entity} /> : 'Loading'}
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
+24
-44
@@ -15,68 +15,49 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Link as RouterLink, generatePath } from 'react-router-dom';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { RollbarProject } from '../../api/types';
|
||||
|
||||
const projectUrl = (org: string, id: number) =>
|
||||
`https://rollbar.com/${org}/all/items/?projects=${id}`;
|
||||
import { entityRouteRef } from '../../routes';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
field: 'id',
|
||||
type: 'numeric',
|
||||
align: 'left',
|
||||
width: '100px',
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
field: 'name',
|
||||
field: 'metadata.name',
|
||||
type: 'string',
|
||||
highlight: true,
|
||||
render: (row: Partial<RollbarProject>) => (
|
||||
<Link component={RouterLink} to={`/rollbar/${row.name}`}>
|
||||
{row.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
field: 'status',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
title: 'Open',
|
||||
width: '10%',
|
||||
render: (row: any) => (
|
||||
render: (entity: any) => (
|
||||
<Link
|
||||
href={projectUrl(row.organization, row.id)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
component={RouterLink}
|
||||
to={generatePath(entityRouteRef.path, {
|
||||
optionalNamespaceAndName: [
|
||||
entity.metadata.namespace,
|
||||
entity.metadata.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
kind: entity.kind,
|
||||
})}
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
{entity.metadata.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
field: 'metadata.description',
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
projects: RollbarProject[];
|
||||
entities: Entity[];
|
||||
loading: boolean;
|
||||
organization: string;
|
||||
error?: any;
|
||||
};
|
||||
|
||||
export const RollbarProjectTable = ({
|
||||
projects,
|
||||
organization,
|
||||
loading,
|
||||
error,
|
||||
}: Props) => {
|
||||
export const RollbarProjectTable = ({ entities, loading, error }: Props) => {
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
@@ -92,14 +73,13 @@ export const RollbarProjectTable = ({
|
||||
isLoading={loading}
|
||||
columns={columns}
|
||||
options={{
|
||||
padding: 'dense',
|
||||
search: true,
|
||||
paging: true,
|
||||
pageSize: 10,
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title="Projects"
|
||||
data={projects.map(p => ({ organization, ...p }))}
|
||||
data={entities}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -16,17 +16,15 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { Box, Link, Typography } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import {
|
||||
RollbarFrameworkId,
|
||||
RollbarLevel,
|
||||
RollbarTopActiveItem,
|
||||
} from '../../api/types';
|
||||
import { RollbarTrendGraph } from '../RollbarTrendGraph/RollbarTrendGraph';
|
||||
|
||||
const itemUrl = (org: string, project: string, id: number) =>
|
||||
`https://rollbar.com/${org}/${project}/items/${id}`;
|
||||
import { buildItemUrl } from '../../utils';
|
||||
import { TrendGraph } from '../TrendGraph/TrendGraph';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
@@ -37,7 +35,7 @@ const columns: TableColumn[] = [
|
||||
width: '70px',
|
||||
render: (data: any) => (
|
||||
<Link
|
||||
href={itemUrl(data.org, data.project, data.item.counter)}
|
||||
href={buildItemUrl(data.org, data.project, data.item.counter)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@@ -54,7 +52,7 @@ const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'Trend',
|
||||
sorting: false,
|
||||
render: (data: any) => <RollbarTrendGraph counts={data.counts} />,
|
||||
render: (data: any) => <TrendGraph counts={data.counts} />,
|
||||
},
|
||||
{
|
||||
title: 'Occurrences',
|
||||
@@ -127,7 +125,12 @@ export const RollbarTopItemsTable = ({
|
||||
pageSize: 5,
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title="Top Active Items"
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box mr={1} />
|
||||
<Typography variant="h6">Top Active Items / {project}</Typography>
|
||||
</Box>
|
||||
}
|
||||
data={items.map(i => ({ org: organization, project, ...i }))}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { WarningPanel } from '@backstage/core';
|
||||
import { catalogRouteRef } from '../routes';
|
||||
import { ROLLBAR_ANNOTATION } from '../constants';
|
||||
import { EntityPageRollbar } from './EntityPageRollbar/EntityPageRollbar';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
entity.metadata.annotations?.[ROLLBAR_ANNOTATION] !== '';
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
export const Router = ({ entity }: Props) =>
|
||||
!isPluginApplicableToEntity(entity) ? (
|
||||
<WarningPanel title="Rollbar plugin:">
|
||||
<pre>
|
||||
entity.metadata.annotations['{ROLLBAR_ANNOTATION}']` key is missing on
|
||||
the entity.
|
||||
</pre>
|
||||
</WarningPanel>
|
||||
) : (
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/${catalogRouteRef.path}`}
|
||||
element={<EntityPageRollbar entity={entity} />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
+3
-5
@@ -17,15 +17,13 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { RollbarTrendGraph } from './RollbarTrendGraph';
|
||||
import { TrendGraph } from './TrendGraph';
|
||||
|
||||
describe('RollbarTrendGraph component', () => {
|
||||
describe('TrendGraph component', () => {
|
||||
it('should render a trend graph sparkline', async () => {
|
||||
const mockCounts = [1, 2, 3, 4];
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
<RollbarTrendGraph counts={mockCounts} data-testid="graph" />,
|
||||
),
|
||||
wrapInTestApp(<TrendGraph counts={mockCounts} data-testid="graph" />),
|
||||
);
|
||||
expect(rendered).toBeTruthy();
|
||||
});
|
||||
+1
-1
@@ -21,7 +21,7 @@ type Props = {
|
||||
counts: number[];
|
||||
};
|
||||
|
||||
export const RollbarTrendGraph = ({ counts }: Props) => {
|
||||
export const TrendGraph = ({ counts }: Props) => {
|
||||
return (
|
||||
<Sparklines data={counts} svgHeight={48} min={0} margin={4}>
|
||||
<SparklinesBars barWidth={2} />
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const ROLLBAR_ANNOTATION = 'rollbar.com/project-slug';
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useAsync } from 'react-use';
|
||||
import { useApi } from '@backstage/core';
|
||||
import {
|
||||
catalogApiRef,
|
||||
useEntityCompoundName,
|
||||
} from '@backstage/plugin-catalog';
|
||||
|
||||
export function useCatalogEntity() {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { namespace, name } = useEntityCompoundName();
|
||||
|
||||
const { value: entity, error, loading } = useAsync(
|
||||
() => catalogApi.getEntityByName({ kind: 'Component', namespace, name }),
|
||||
[catalogApi, namespace, name],
|
||||
);
|
||||
|
||||
return { entity, error, loading };
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useApi, configApiRef } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ROLLBAR_ANNOTATION } from '../constants';
|
||||
|
||||
export function useProjectSlugFromEntity(entity: Entity) {
|
||||
const configApi = useApi(configApiRef);
|
||||
|
||||
const [project, organization] = (
|
||||
entity?.metadata?.annotations?.[ROLLBAR_ANNOTATION] ?? ''
|
||||
)
|
||||
.split('/')
|
||||
.reverse();
|
||||
|
||||
return {
|
||||
project,
|
||||
organization:
|
||||
organization ??
|
||||
configApi.getOptionalString('rollbar.organization') ??
|
||||
configApi.getString('organization.name'),
|
||||
};
|
||||
}
|
||||
+16
-20
@@ -14,29 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { configApiRef, useApi } from '@backstage/core';
|
||||
import { rollbarApiRef } from '../../api/RollbarApi';
|
||||
import { RollbarLayout } from '../RollbarLayout/RollbarLayout';
|
||||
import { RollbarProjectTable } from './RollbarProjectTable';
|
||||
import { useApi, configApiRef } from '@backstage/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { ROLLBAR_ANNOTATION } from '../constants';
|
||||
|
||||
export const RollbarPage = () => {
|
||||
export function useRollbarEntities() {
|
||||
const configApi = useApi(configApiRef);
|
||||
const rollbarApi = useApi(rollbarApiRef);
|
||||
const org =
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const organization =
|
||||
configApi.getOptionalString('rollbar.organization') ??
|
||||
configApi.getString('organization.name');
|
||||
const { value, loading, error } = useAsync(() => rollbarApi.getAllProjects());
|
||||
|
||||
return (
|
||||
<RollbarLayout>
|
||||
<RollbarProjectTable
|
||||
projects={value || []}
|
||||
organization={org}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
</RollbarLayout>
|
||||
);
|
||||
};
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
const entities = await catalogApi.getEntities();
|
||||
return entities.filter(entity => {
|
||||
return !!entity.metadata.annotations?.[ROLLBAR_ANNOTATION];
|
||||
});
|
||||
}, [catalogApi]);
|
||||
|
||||
return { entities: value, organization, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useAsync } from 'react-use';
|
||||
import { useApi } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { rollbarApiRef } from '../api';
|
||||
import { RollbarTopActiveItem } from '../api/types';
|
||||
import { useProjectSlugFromEntity } from './useProject';
|
||||
|
||||
export function useTopActiveItems(entity: Entity) {
|
||||
const api = useApi(rollbarApiRef);
|
||||
const { organization, project } = useProjectSlugFromEntity(entity);
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
if (!project) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return api
|
||||
.getTopActiveItems(project, 168)
|
||||
.then(data =>
|
||||
data.sort((a, b) => b.item.occurrences - a.item.occurrences),
|
||||
);
|
||||
}, [api, organization, project, entity]);
|
||||
|
||||
return {
|
||||
items: value as RollbarTopActiveItem[],
|
||||
organization,
|
||||
project,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export * from './api/RollbarApi';
|
||||
export { RollbarClient } from './api/RollbarClient';
|
||||
export { RollbarMockClient } from './api/RollbarMockClient';
|
||||
export * from './api';
|
||||
export * from './routes';
|
||||
export { Router } from './components/Router';
|
||||
export { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage';
|
||||
export { EntityPageRollbar } from './components/EntityPageRollbar/EntityPageRollbar';
|
||||
export { ROLLBAR_ANNOTATION } from './constants';
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import { RollbarPage } from './components/RollbarPage/RollbarPage';
|
||||
import { rootRouteRef, entityRouteRef } from './routes';
|
||||
import { RollbarHome } from './components/RollbarHome/RollbarHome';
|
||||
import { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage';
|
||||
import { rootRoute, rootProjectRoute } from './routes';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'rollbar',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRoute, RollbarPage);
|
||||
router.addRoute(rootProjectRoute, RollbarProjectPage);
|
||||
router.addRoute(rootRouteRef, RollbarHome);
|
||||
router.addRoute(entityRouteRef, RollbarProjectPage);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,12 +16,21 @@
|
||||
|
||||
import { createRouteRef } from '@backstage/core';
|
||||
|
||||
export const rootRoute = createRouteRef({
|
||||
const NoIcon = () => null;
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '/rollbar',
|
||||
title: 'Rollbar Home',
|
||||
});
|
||||
|
||||
export const rootProjectRoute = createRouteRef({
|
||||
path: '/rollbar/:componentId/*',
|
||||
export const entityRouteRef = createRouteRef({
|
||||
path: '/rollbar/:optionalNamespaceAndName',
|
||||
title: 'Rollbar',
|
||||
});
|
||||
|
||||
export const catalogRouteRef = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '',
|
||||
title: 'Rollbar',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const buildProjectUrl = (org: string, id: number) =>
|
||||
`https://rollbar.com/${org}/all/items/?projects=${id}`;
|
||||
|
||||
export const buildItemUrl = (org: string, project: string, id: number) =>
|
||||
`https://rollbar.com/${org}/${project}/items/${id}`;
|
||||
@@ -1,4 +1,5 @@
|
||||
# Title
|
||||
|
||||
Welcome to the scaffolder plugin!
|
||||
|
||||
## Sub-section 1
|
||||
|
||||
-1
@@ -5,7 +5,6 @@ metadata:
|
||||
description: {{cookiecutter.description}}
|
||||
annotations:
|
||||
github.com/project-slug: {{cookiecutter.storePath}}
|
||||
backstage.io/github-actions-id: {{cookiecutter.storePath}}
|
||||
backstage.io/techdocs-ref: github:https://github.com/{{cookiecutter.storePath}}
|
||||
spec:
|
||||
type: website
|
||||
|
||||
@@ -2,11 +2,12 @@ apiVersion: backstage.io/v1alpha1
|
||||
kind: Template
|
||||
metadata:
|
||||
name: springboot-template
|
||||
title: Spring Boot GRPC Service
|
||||
title: Spring Boot gRPC Service
|
||||
description: Create a simple microservice using gRPC and Spring Boot Java
|
||||
tags:
|
||||
- recommended
|
||||
- java
|
||||
- grpc
|
||||
spec:
|
||||
owner: service@example.com
|
||||
templater: cookiecutter
|
||||
@@ -29,4 +30,4 @@ spec:
|
||||
title: Port
|
||||
type: integer
|
||||
default: 8080
|
||||
description: The port to run the GRPC service on
|
||||
description: The port to run the gRPC service on
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ metadata:
|
||||
description: {{cookiecutter.description}}
|
||||
annotations:
|
||||
github.com/project-slug: {{cookiecutter.storePath}}
|
||||
backstage.io/github-actions-id: {{cookiecutter.storePath}}
|
||||
backstage.io/techdocs-ref: github:https://github.com/{{cookiecutter.storePath}}
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
## {{ cookiecutter.component_id }}
|
||||
|
||||
{{ cookiecutter.description }}
|
||||
|
||||
## Getting started
|
||||
|
||||
Start write your documentation by adding more markdown (.md) files to this folder (/docs) or replace the content in this file.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
The Table of Contents on the right is generated automatically based on the hierarchy
|
||||
of headings. Only use one H1 (`#` in Markdown) per file.
|
||||
|
||||
## Site navigation
|
||||
|
||||
For new pages to appear in the left hand navigation you need edit the `mkdocs.yml`
|
||||
file in root of your repo. The navigation can also link out to other sites.
|
||||
|
||||
Alternatively, if there is no `nav` section in `mkdocs.yml`, a navigation section
|
||||
will be created for you. However, you will not be able to use alternate titles for
|
||||
pages, or include links to other sites.
|
||||
|
||||
Note that MkDocs uses `mkdocs.yml`, not `mkdocs.yaml`, although both appear to work.
|
||||
See also <https://www.mkdocs.org/user-guide/configuration/>.
|
||||
|
||||
## Support
|
||||
|
||||
That's it. If you need support, reach out in [#docs-like-code](https://discord.com/channels/687207715902193673/714754240933003266) on Discord.
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
site_name: {{cookiecutter.component_id}}
|
||||
site_description: {{cookiecutter.description}}
|
||||
|
||||
nav:
|
||||
- Introduction: index.md
|
||||
|
||||
plugins:
|
||||
- techdocs-core
|
||||
@@ -16,4 +16,3 @@
|
||||
|
||||
export * from './scaffolder';
|
||||
export * from './service/router';
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@ describe('GitHubPreparer', () => {
|
||||
mockEntity.spec.path = './template/test/1/2/3';
|
||||
const response = await preparer.prepare(mockEntity);
|
||||
|
||||
expect(response).toMatch(new RegExp(/\/template\/test\/1\/2\/3$/));
|
||||
expect(response.split('\\').join('/')).toMatch(
|
||||
/\/template\/test\/1\/2\/3$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,13 @@ import Docker from 'dockerode';
|
||||
import { runDockerContainer } from './helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
if (process.platform === 'win32') {
|
||||
// eslint-disable-next-line jest/no-focused-tests
|
||||
it.only('should skip tests on windows', () => {
|
||||
expect('test').not.toBe('run');
|
||||
});
|
||||
}
|
||||
|
||||
const mockDocker = new Docker() as jest.Mocked<Docker>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -26,7 +26,7 @@ export function getRequestHeaders(token: string) {
|
||||
}
|
||||
|
||||
export function getSentryApiForwarder(token: string, logger: Logger) {
|
||||
return function fowardRequest(
|
||||
return function forwardRequest(
|
||||
request: express.Request,
|
||||
response: express.Response,
|
||||
) {
|
||||
|
||||
@@ -17,6 +17,13 @@ import { DirectoryPreparer } from './dir';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { checkoutGitRepository } from './helpers';
|
||||
|
||||
function normalizePath(path: string) {
|
||||
return path
|
||||
.replace(/^[a-z]:/i, '')
|
||||
.split('\\')
|
||||
.join('/');
|
||||
}
|
||||
|
||||
jest.mock('./helpers', () => ({
|
||||
...jest.requireActual<{}>('./helpers'),
|
||||
checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch/'),
|
||||
@@ -47,7 +54,7 @@ describe('directory preparer', () => {
|
||||
'backstage.io/techdocs-ref': 'dir:./our-documentation',
|
||||
});
|
||||
|
||||
expect(await directoryPreparer.prepare(mockEntity)).toEqual(
|
||||
expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual(
|
||||
'/directory/our-documentation',
|
||||
);
|
||||
});
|
||||
@@ -61,7 +68,7 @@ describe('directory preparer', () => {
|
||||
'backstage.io/techdocs-ref': 'dir:/our-documentation/techdocs',
|
||||
});
|
||||
|
||||
expect(await directoryPreparer.prepare(mockEntity)).toEqual(
|
||||
expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual(
|
||||
'/our-documentation/techdocs',
|
||||
);
|
||||
});
|
||||
@@ -75,7 +82,7 @@ describe('directory preparer', () => {
|
||||
'backstage.io/techdocs-ref': 'dir:./docs',
|
||||
});
|
||||
|
||||
expect(await directoryPreparer.prepare(mockEntity)).toEqual(
|
||||
expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual(
|
||||
'/tmp/backstage-repo/org/name/branch/docs',
|
||||
);
|
||||
expect(checkoutGitRepository).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -18,6 +18,13 @@ import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { GithubPreparer } from './github';
|
||||
import { checkoutGithubRepository } from './helpers';
|
||||
|
||||
function normalizePath(path: string) {
|
||||
return path
|
||||
.replace(/^[a-z]:/i, '')
|
||||
.split('\\')
|
||||
.join('/');
|
||||
}
|
||||
|
||||
jest.mock('./helpers', () => ({
|
||||
...jest.requireActual<{}>('./helpers'),
|
||||
checkoutGithubRepository: jest.fn(
|
||||
@@ -51,7 +58,7 @@ describe('github preparer', () => {
|
||||
|
||||
const tempDocsPath = await preparer.prepare(mockEntity);
|
||||
expect(checkoutGithubRepository).toHaveBeenCalledTimes(1);
|
||||
expect(tempDocsPath).toEqual(
|
||||
expect(normalizePath(tempDocsPath)).toEqual(
|
||||
'/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component',
|
||||
);
|
||||
});
|
||||
|
||||
+8
-11
@@ -16,19 +16,16 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Reader } from '@backstage/plugin-techdocs';
|
||||
import { Content } from '@backstage/core';
|
||||
import { Reader } from './reader';
|
||||
|
||||
export const EntityPageDocs = ({ entity }: { entity: Entity }) => {
|
||||
return (
|
||||
<Content>
|
||||
<Reader
|
||||
entityId={{
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace,
|
||||
name: entity.metadata.name,
|
||||
}}
|
||||
/>
|
||||
</Content>
|
||||
<Reader
|
||||
entityId={{
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace,
|
||||
name: entity.metadata.name,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
import {
|
||||
rootRouteRef,
|
||||
rootDocsRouteRef,
|
||||
rootCatalogDocsRouteRef,
|
||||
} from './plugin';
|
||||
import { TechDocsHome } from './reader/components/TechDocsHome';
|
||||
import { TechDocsPage } from './reader/components/TechDocsPage';
|
||||
import { EntityPageDocs } from './EntityPageDocs';
|
||||
|
||||
export const Router = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path={`/${rootRouteRef.path}`} element={<TechDocsHome />} />
|
||||
<Route path={`/${rootDocsRouteRef.path}`} element={<TechDocsPage />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmbeddedDocsRouter = ({ entity }: { entity: Entity }) => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/${rootCatalogDocsRouteRef.path}`}
|
||||
element={<EntityPageDocs entity={entity} />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
@@ -42,7 +42,9 @@ export class TechDocsStorageApi implements TechDocsStorage {
|
||||
async getEntityDocs(entityId: ParsedEntityId, path: string) {
|
||||
const { kind, namespace, name } = entityId;
|
||||
|
||||
const url = `${this.apiOrigin}/${kind}/${namespace ? namespace : 'default'}/${name}/${path}`;
|
||||
const url = `${this.apiOrigin}/${kind}/${
|
||||
namespace ? namespace : 'default'
|
||||
}/${name}/${path}`;
|
||||
|
||||
const request = await fetch(
|
||||
`${url.endsWith('/') ? url : `${url}/`}index.html`,
|
||||
@@ -64,7 +66,9 @@ export class TechDocsStorageApi implements TechDocsStorage {
|
||||
|
||||
return new URL(
|
||||
oldBaseUrl,
|
||||
`${this.apiOrigin}/${kind}/${namespace ? namespace : 'default'}/${name}/${path}`,
|
||||
`${this.apiOrigin}/${kind}/${
|
||||
namespace ? namespace : 'default'
|
||||
}/${name}/${path}`,
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export { Router, EmbeddedDocsRouter } from './Router';
|
||||
export * from './reader';
|
||||
export * from './api';
|
||||
|
||||
@@ -30,23 +30,22 @@
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import { TechDocsHome } from './reader/components/TechDocsHome';
|
||||
import { TechDocsPage } from './reader/components/TechDocsPage';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/docs',
|
||||
path: '',
|
||||
title: 'TechDocs Landing Page',
|
||||
});
|
||||
|
||||
export const rootDocsRouteRef = createRouteRef({
|
||||
path: '/docs/:entityId/*',
|
||||
path: ':entityId/*',
|
||||
title: 'Docs',
|
||||
});
|
||||
|
||||
export const rootCatalogDocsRouteRef = createRouteRef({
|
||||
path: '*',
|
||||
title: 'Docs',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'techdocs',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, TechDocsHome);
|
||||
router.addRoute(rootDocsRouteRef, TechDocsPage);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, generatePath } from 'react-router-dom';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { ItemCard, Progress, useApi } from '@backstage/core';
|
||||
import { TechDocsPageWrapper } from './TechDocsPageWrapper';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { rootDocsRouteRef } from '../../plugin';
|
||||
|
||||
export const TechDocsHome = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
@@ -67,9 +68,11 @@ export const TechDocsHome = () => {
|
||||
<ItemCard
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/docs/${entity.kind}:${
|
||||
entity.metadata.namespace ?? ''
|
||||
}:${entity.metadata.name}`,
|
||||
generatePath(rootDocsRouteRef.path, {
|
||||
entityId: `${entity.kind}:${
|
||||
entity.metadata.namespace ?? ''
|
||||
}:${entity.metadata.name}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
title={entity.metadata.name}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Title
|
||||
|
||||
Welcome to the welcome plugin!
|
||||
|
||||
## Sub-section 1
|
||||
|
||||
Reference in New Issue
Block a user