Merge remote-tracking branch 'upstream/master' into feat/relative-ref-tmp

This commit is contained in:
Dominik Henneke
2021-07-21 12:16:14 +02:00
118 changed files with 1736 additions and 939 deletions
+9 -7
View File
@@ -59,15 +59,17 @@ const apiPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
<Grid item xs={12} md={6}>
<EntityAboutCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item md={6}>
<EntityConsumingComponentsCard />
<Grid container>
<Grid item md={12}>
<Grid item xs={12} md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</Grid>
+4 -2
View File
@@ -52,14 +52,16 @@ const apiDocsPlugin: BackstagePlugin<
export { apiDocsPlugin };
export { apiDocsPlugin as plugin };
// Warning: (ae-forgotten-export) The symbol "ApiExplorerPageProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ApiExplorerPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const ApiExplorerPage: ({
initiallySelectedFilter,
columns,
}: ApiExplorerPageProps) => JSX.Element;
}: {
initiallySelectedFilter?: UserListFilterKind | undefined;
columns?: TableColumn<CatalogTableRow>[] | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "ApiTypeTitle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -1,40 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Header, Page } from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
type Props = {
children?: React.ReactNode;
};
export const ApiExplorerLayout = ({ children }: Props) => {
const configApi = useApi(configApiRef);
const generatedSubtitle = `${
configApi.getOptionalString('organization.name') ?? 'Backstage'
} API Explorer`;
return (
<Page themeId="apis">
<Header
title="APIs"
subtitle={generatedSubtitle}
pageTitleOverride="APIs"
/>
{children}
</Page>
);
};
@@ -14,6 +14,21 @@
* limitations under the License.
*/
import {
Content,
ContentHeader,
PageWithHeader,
SupportButton,
TableColumn,
} from '@backstage/core-components';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
CatalogTable,
CatalogTableRow,
FilteredEntityLayout,
EntityListContainer,
FilterContainer,
} from '@backstage/plugin-catalog';
import {
EntityKindPicker,
EntityLifecyclePicker,
@@ -24,29 +39,10 @@ import {
UserListFilterKind,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import { CatalogTable, CatalogTableRow } from '@backstage/plugin-catalog';
import { Button, makeStyles } from '@material-ui/core';
import { Button } from '@material-ui/core';
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { createComponentRouteRef } from '../../routes';
import { ApiExplorerLayout } from './ApiExplorerLayout';
import {
Content,
ContentHeader,
SupportButton,
TableColumn,
} from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
const useStyles = makeStyles(theme => ({
contentWrapper: {
display: 'grid',
gridTemplateAreas: "'filters' 'table'",
gridTemplateColumns: '250px 1fr',
gridColumnGap: theme.spacing(2),
},
}));
const defaultColumns: TableColumn<CatalogTableRow>[] = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
@@ -58,7 +54,7 @@ const defaultColumns: TableColumn<CatalogTableRow>[] = [
CatalogTable.columns.createTagsColumn(),
];
export type ApiExplorerPageProps = {
type ApiExplorerPageProps = {
initiallySelectedFilter?: UserListFilterKind;
columns?: TableColumn<CatalogTableRow>[];
};
@@ -67,11 +63,19 @@ export const ApiExplorerPage = ({
initiallySelectedFilter = 'all',
columns,
}: ApiExplorerPageProps) => {
const styles = useStyles();
const createComponentLink = useRouteRef(createComponentRouteRef);
const configApi = useApi(configApiRef);
const generatedSubtitle = `${
configApi.getOptionalString('organization.name') ?? 'Backstage'
} API Explorer`;
return (
<ApiExplorerLayout>
<PageWithHeader
themeId="apis"
title="APIs"
subtitle={generatedSubtitle}
pageTitleOverride="APIs"
>
<Content>
<ContentHeader title="">
{createComponentLink && (
@@ -86,20 +90,22 @@ export const ApiExplorerPage = ({
)}
<SupportButton>All your APIs</SupportButton>
</ContentHeader>
<div className={styles.contentWrapper}>
<EntityListProvider>
<div>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="api" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</div>
<CatalogTable columns={columns || defaultColumns} />
</EntityListProvider>
</div>
</FilterContainer>
<EntityListContainer>
<CatalogTable columns={columns || defaultColumns} />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</ApiExplorerLayout>
</PageWithHeader>
);
};
+12 -1
View File
@@ -346,6 +346,16 @@ export interface RouterOptions {
providerFactories?: ProviderFactories;
}
// Warning: (ae-missing-release-tag) "TokenIssuer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type TokenIssuer = {
issueToken(params: TokenParams): Promise<string>;
listPublicKeys(): Promise<{
keys: AnyJWK[];
}>;
};
// Warning: (ae-missing-release-tag) "verifyNonce" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -366,9 +376,10 @@ export type WebMessageResponse =
// Warnings were encountered during analysis:
//
// src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
// src/providers/google/provider.d.ts:36:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:105:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:108:5 - (ae-forgotten-export) The symbol "TokenIssuer" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:111:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:128:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
+1
View File
@@ -16,6 +16,7 @@
export * from './service/router';
export { IdentityClient } from './identity';
export type { TokenIssuer } from './identity';
export * from './providers';
// flow package provides 2 functions
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { Grid, Typography } from '@material-ui/core';
import { Chip, Grid, Typography } from '@material-ui/core';
import React from 'react';
import { ImportStepper } from './ImportStepper';
import { StepperProviderOpts } from './ImportStepper/defaults';
import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import {
Content,
ContentHeader,
@@ -29,30 +29,12 @@ import {
SupportButton,
} from '@backstage/core-components';
function repositories(configApi: ConfigApi): string[] {
const integrations = configApi.getConfig('integrations');
const repos = [];
if (integrations.has('github')) {
repos.push('GitHub');
}
if (integrations.has('bitbucket')) {
repos.push('Bitbucket');
}
if (integrations.has('gitlab')) {
repos.push('GitLab');
}
if (integrations.has('azure')) {
repos.push('Azure');
}
return repos;
}
export const ImportComponentPage = (opts: StepperProviderOpts) => {
const configApi = useApi(configApiRef);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const repos = repositories(configApi);
const repositoryString = repos.join(', ').replace(/, (\w*)$/, ' or $1');
const integrations = configApi.getConfig('integrations');
const hasGithubIntegration = integrations.has('github');
return (
<Page themeId="home">
@@ -76,7 +58,8 @@ export const ImportComponentPage = (opts: StepperProviderOpts) => {
}}
>
<Typography variant="body2" paragraph>
Enter the URL to your SCM repository to add it to {appTitle}.
Enter the URL to your source code repository to add it to{' '}
{appTitle}.
</Typography>
<Typography variant="h6">
Link to an existing entity file
@@ -91,10 +74,11 @@ export const ImportComponentPage = (opts: StepperProviderOpts) => {
The wizard analyzes the file, previews the entities, and adds
them to the {appTitle} catalog.
</Typography>
{repos.length > 0 && (
{hasGithubIntegration && (
<>
<Typography variant="h6">
Link to a {repositoryString} repository
Link to a repository{' '}
<Chip label="GitHub only" variant="outlined" size="small" />
</Typography>
<Typography
variant="subtitle2"
@@ -52,8 +52,8 @@ export const MockEntityListContextProvider = ({
const defaultContext: EntityListContextProps = {
entities: [],
backendEntities: [],
updateFilters: updateFilters,
filters: filters,
updateFilters,
filters,
loading: false,
queryParameters: {},
};
+80 -9
View File
@@ -5,12 +5,21 @@
```ts
/// <reference types="react" />
import { AddLocationRequest } from '@backstage/catalog-client';
import { AddLocationResponse } from '@backstage/catalog-client';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogClient } from '@backstage/catalog-client';
import { CatalogEntitiesRequest } from '@backstage/catalog-client';
import { CatalogListResponse } from '@backstage/catalog-client';
import { CatalogRequestOptions } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { IdentityApi } from '@backstage/core-plugin-api';
import { InfoCardVariants } from '@backstage/core-components';
import { Location as Location_2 } from '@backstage/catalog-model';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
@@ -30,7 +39,7 @@ export function AboutCard({ variant }: AboutCardProps): JSX.Element;
// Warning: (ae-missing-release-tag) "AboutContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const AboutContent: ({ entity }: Props_2) => JSX.Element;
export const AboutContent: ({ entity }: Props) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "AboutField" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -41,7 +50,54 @@ export const AboutField: ({
value,
gridSizes,
children,
}: Props_3) => JSX.Element;
}: Props_2) => JSX.Element;
// Warning: (ae-missing-release-tag) "CatalogClientWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export class CatalogClientWrapper implements CatalogApi {
constructor(options: { client: CatalogClient; identityApi: IdentityApi });
// (undocumented)
addLocation(
request: AddLocationRequest,
options?: CatalogRequestOptions,
): Promise<AddLocationResponse>;
// (undocumented)
getEntities(
request?: CatalogEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<CatalogListResponse<Entity>>;
// (undocumented)
getEntityByName(
compoundName: EntityName,
options?: CatalogRequestOptions,
): Promise<Entity | undefined>;
// (undocumented)
getLocationByEntity(
entity: Entity,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
getLocationById(
id: string,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
getOriginLocationByEntity(
entity: Entity,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
removeEntityByUid(
uid: string,
options?: CatalogRequestOptions,
): Promise<void>;
// (undocumented)
removeLocationById(
id: string,
options?: CatalogRequestOptions,
): Promise<void>;
}
// Warning: (ae-missing-release-tag) "CatalogEntityPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -53,17 +109,11 @@ export const CatalogEntityPage: () => JSX.Element;
//
// @public (undocumented)
export const CatalogIndexPage: ({
initiallySelectedFilter,
columns,
actions,
initiallySelectedFilter,
}: CatalogPageProps) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "CatalogLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const CatalogLayout: ({ children }: Props) => JSX.Element;
// Warning: (ae-missing-release-tag) "catalogPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -249,6 +299,13 @@ export const EntityLinksCard: ({
variant?: 'gridItem' | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "EntityListContainer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityListContainer: ({
children,
}: PropsWithChildren<{}>) => JSX.Element;
// Warning: (ae-missing-release-tag) "EntityOrphanWarning" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -287,6 +344,20 @@ export const EntitySwitch: {
// @public (undocumented)
export const EntitySystemDiagramCard: SystemDiagramCard;
// Warning: (ae-missing-release-tag) "FilterContainer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const FilterContainer: ({
children,
}: PropsWithChildren<{}>) => JSX.Element;
// Warning: (ae-missing-release-tag) "FilteredEntityLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const FilteredEntityLayout: ({
children,
}: PropsWithChildren<{}>) => JSX.Element;
// Warning: (ae-missing-release-tag) "isComponentType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+2 -5
View File
@@ -19,16 +19,13 @@ import {
AddLocationRequest,
AddLocationResponse,
CatalogApi,
CatalogClient,
CatalogEntitiesRequest,
CatalogListResponse,
CatalogClient,
CatalogRequestOptions,
} from '@backstage/catalog-client';
import { IdentityApi } from '@backstage/core-plugin-api';
type CatalogRequestOptions = {
token?: string;
};
/**
* CatalogClient wrapper that injects identity token for all requests
*/
@@ -1,42 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { Header, Page } from '@backstage/core-components';
type Props = {
children?: React.ReactNode;
};
export const CatalogLayout = ({ children }: Props) => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
return (
<Page themeId="home">
<Header
title={`${orgName} Catalog`}
subtitle={`Catalog of software components at ${orgName}`}
pageTitleOverride="Home"
/>
{children}
</Page>
);
};
export default CatalogLayout;
@@ -26,6 +26,7 @@ import {
MockStorageApi,
renderWithEffects,
wrapInTestApp,
mockBreakpoint,
} from '@backstage/test-utils';
import { fireEvent, screen } from '@testing-library/react';
import React from 'react';
@@ -244,4 +245,13 @@ describe('CatalogPage', () => {
screen.findByText(/Starred \(1\)/),
).resolves.toBeInTheDocument();
});
it('should wrap filter in drawer on smaller screens', async () => {
mockBreakpoint({ matches: true });
const { getByRole } = await renderWrapped(<CatalogPage />);
const button = getByRole('button', { name: 'Filters' });
expect(getByRole('presentation', { hidden: true })).toBeInTheDocument();
fireEvent.click(button);
expect(getByRole('presentation')).toBeVisible();
});
});
@@ -14,8 +14,15 @@
* limitations under the License.
*/
import React from 'react';
import { Grid } from '@material-ui/core';
import {
Content,
ContentHeader,
PageWithHeader,
SupportButton,
TableColumn,
TableProps,
} from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import {
EntityKindPicker,
EntityLifecyclePicker,
@@ -26,18 +33,15 @@ import {
UserListFilterKind,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import React from 'react';
import { CatalogTable } from '../CatalogTable';
import { EntityRow } from '../CatalogTable/types';
import CatalogLayout from './CatalogLayout';
import { CreateComponentButton } from '../CreateComponentButton';
import {
Content,
ContentHeader,
SupportButton,
TableColumn,
TableProps,
} from '@backstage/core-components';
FilteredEntityLayout,
EntityListContainer,
FilterContainer,
} from '../FilteredEntityLayout';
export type CatalogPageProps = {
initiallySelectedFilter?: UserListFilterKind;
@@ -46,39 +50,36 @@ export type CatalogPageProps = {
};
export const CatalogPage = ({
initiallySelectedFilter = 'owned',
columns,
actions,
}: CatalogPageProps) => (
<CatalogLayout>
<Content>
<ContentHeader title="Components">
<CreateComponentButton />
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<Grid container spacing={2}>
initiallySelectedFilter = 'owned',
}: CatalogPageProps) => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
return (
<PageWithHeader title={`${orgName} Catalog`} themeId="home">
<Content>
<ContentHeader title="Components">
<CreateComponentButton />
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<EntityListProvider>
<Grid item sm={12} lg={2} alignContent="flex-start">
<Grid container>
<Grid item xs={12} sm={4} lg={12}>
<EntityKindPicker initialFilter="component" hidden />
<EntityTypePicker />
</Grid>
<Grid item xs={12} sm={4} lg={12}>
<UserListPicker initialFilter={initiallySelectedFilter} />
</Grid>
<Grid item xs={12} sm={4} lg={12}>
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sm={12} lg={10}>
<CatalogTable columns={columns} actions={actions} />
</Grid>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="component" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<CatalogTable columns={columns} actions={actions} />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Grid>
</Content>
</CatalogLayout>
);
</Content>
</PageWithHeader>
);
};
@@ -13,5 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CatalogLayout } from './CatalogLayout';
export { CatalogPage } from './CatalogPage';
@@ -23,9 +23,7 @@ import { useRouteRef } from '@backstage/core-plugin-api';
export const CreateComponentButton = () => {
const createComponentLink = useRouteRef(createComponentRouteRef);
if (!createComponentLink) return null;
return (
return createComponentLink ? (
<Button
component={RouterLink}
variant="contained"
@@ -34,5 +32,5 @@ export const CreateComponentButton = () => {
>
Create Component
</Button>
);
) : null;
};
@@ -13,4 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CreateComponentButton } from './CreateComponentButton';
@@ -20,9 +20,17 @@ import {
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import {
useElementFilter,
Content,
Header,
HeaderLabel,
Page,
Progress,
RoutedTabs,
} from '@backstage/core-components';
import {
attachComponentData,
IconComponent,
useElementFilter,
} from '@backstage/core-plugin-api';
import {
EntityContext,
@@ -37,14 +45,6 @@ import { useNavigate } from 'react-router';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
import {
Content,
Header,
HeaderLabel,
Page,
Progress,
RoutedTabs,
} from '@backstage/core-components';
type SubRoute = {
path: string;
@@ -68,12 +68,21 @@ const EntityLayoutTitle = ({
}: {
title: string;
entity: Entity | undefined;
}) => (
<Box display="inline-flex" alignItems="center" height="1em">
{title}
{entity && <FavouriteEntity entity={entity} />}
</Box>
);
}) => {
return (
<Box display="inline-flex" alignItems="center" height="1em" maxWidth="100%">
<Box
component="span"
textOverflow="ellipsis"
whiteSpace="nowrap"
overflow="hidden"
>
{title}
</Box>
{entity && <FavouriteEntity entity={entity} />}
</Box>
);
};
const headerProps = (
paramKind: string | undefined,
@@ -0,0 +1,24 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Grid } from '@material-ui/core';
import React, { PropsWithChildren } from 'react';
export const EntityListContainer = ({ children }: PropsWithChildren<{}>) => (
<Grid item xs={12} lg={10}>
{children}
</Grid>
);
@@ -0,0 +1,71 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BackstageTheme } from '@backstage/theme';
import {
Box,
Button,
Drawer,
Grid,
Typography,
useMediaQuery,
useTheme,
} from '@material-ui/core';
import FilterListIcon from '@material-ui/icons/FilterList';
import React, { useState, PropsWithChildren } from 'react';
export const FilterContainer = ({ children }: PropsWithChildren<{}>) => {
const isMidSizeScreen = useMediaQuery<BackstageTheme>(theme =>
theme.breakpoints.down('md'),
);
const theme = useTheme<BackstageTheme>();
const [filterDrawerOpen, setFilterDrawerOpen] = useState<boolean>(false);
return isMidSizeScreen ? (
<>
<Button
style={{ marginTop: theme.spacing(1), marginLeft: theme.spacing(1) }}
onClick={() => setFilterDrawerOpen(true)}
startIcon={<FilterListIcon />}
>
Filters
</Button>
<Drawer
open={filterDrawerOpen}
onClose={() => setFilterDrawerOpen(false)}
anchor="left"
disableAutoFocus
keepMounted
variant="temporary"
>
<Box m={2}>
<Typography
variant="h6"
component="h2"
style={{ marginBottom: theme.spacing(1) }}
>
Filters
</Typography>
{children}
</Box>
</Drawer>
</>
) : (
<Grid item lg={2}>
{children}
</Grid>
);
};
@@ -0,0 +1,24 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Grid } from '@material-ui/core';
import React, { PropsWithChildren } from 'react';
export const FilteredEntityLayout = ({ children }: PropsWithChildren<{}>) => (
<Grid container style={{ position: 'relative' }}>
{children}
</Grid>
);
@@ -0,0 +1,19 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { FilteredEntityLayout } from './FilteredEntityLayout';
export { FilterContainer } from './FilterContainer';
export { EntityListContainer } from './EntityListContainer';
+8 -7
View File
@@ -14,16 +14,18 @@
* limitations under the License.
*/
export { CatalogClientWrapper } from './CatalogClientWrapper';
export * from './components/AboutCard';
export { CatalogLayout } from './components/CatalogPage';
export { CatalogResultListItem } from './components/CatalogResultListItem';
export * from './components/CatalogResultListItem';
export { CatalogTable } from './components/CatalogTable';
export type { EntityRow as CatalogTableRow } from './components/CatalogTable';
export { CreateComponentButton } from './components/CreateComponentButton';
export { EntityLayout } from './components/EntityLayout';
export * from './components/CatalogTable/columns';
export * from './components/CreateComponentButton';
export * from './components/EntityLayout';
export * from './components/EntityOrphanWarning';
export { EntityPageLayout } from './components/EntityPageLayout';
export * from './components/EntityPageLayout';
export * from './components/EntitySwitch';
export * from './components/FilteredEntityLayout';
export { Router } from './components/Router';
export {
CatalogEntityPage,
@@ -31,8 +33,8 @@ export {
catalogPlugin,
catalogPlugin as plugin,
EntityAboutCard,
EntityDependsOnComponentsCard,
EntityDependencyOfComponentsCard,
EntityDependsOnComponentsCard,
EntityDependsOnResourcesCard,
EntityHasComponentsCard,
EntityHasResourcesCard,
@@ -41,4 +43,3 @@ export {
EntityLinksCard,
EntitySystemDiagramCard,
} from './plugin';
export * from './components/CatalogTable/columns';
@@ -7,6 +7,7 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
// Warning: (ae-forgotten-export) The symbol "GitReleaseApi" needs to be exported by the entry point index.d.ts
+60 -16
View File
@@ -16,7 +16,7 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { Box, Typography } from '@material-ui/core';
import { Box, Button, Typography } from '@material-ui/core';
import { gitReleaseManagerPlugin, GitReleaseManagerPage } from '../src/plugin';
import { InfoCardPlus } from '../src/components/InfoCardPlus';
@@ -44,10 +44,16 @@ createDevApp()
<Box padding={4}>
<InfoCardPlus>
<Typography variant="h4">Dev notes</Typography>
<Typography>
Configure plugin statically by passing props to the
`GitHubReleaseManagerPage` component
</Typography>
<Typography variant="body2">
Note that the static configuration points towards private
repositories and will thus not work for everyone.
</Typography>
</InfoCardPlus>
<GitReleaseManagerPage
@@ -67,8 +73,10 @@ createDevApp()
<Box padding={4}>
<InfoCardPlus>
<Typography variant="h4">Dev notes</Typography>
<Typography>Each feature can be omitted</Typography>
<Typography>Success callbacks can also be added</Typography>
<Typography>
Each feature can be individually omitted as well as have success
callback attached to them
</Typography>
</InfoCardPlus>
<GitReleaseManagerPage
@@ -79,22 +87,12 @@ createDevApp()
}}
features={{
createRc: {
onSuccess: ({
comparisonUrl,
createdTag,
gitReleaseName,
gitReleaseUrl,
previousTag,
}) => {
onSuccess: args => {
// eslint-disable-next-line no-console
console.log(
'Custom success callback for Create RC',
comparisonUrl,
createdTag,
gitReleaseName,
gitReleaseUrl,
previousTag,
'Custom success callback for Create RC with the following args',
);
console.log(JSON.stringify(args, null, 2)); // eslint-disable-line no-console
},
},
promoteRc: {
@@ -108,4 +106,50 @@ createDevApp()
</Box>
),
})
.addPage({
title: 'Custom',
path: '/custom',
element: (
<Box padding={4}>
<InfoCardPlus>
<Typography variant="h4">Dev notes</Typography>
<Typography>
The custom feature's return value can either be a React Element or
an array of React Elements.
</Typography>
</InfoCardPlus>
<GitReleaseManagerPage
project={{
owner: 'eengervall-playground',
repo: 'playground-semver',
versioningStrategy: 'semver',
}}
features={{
custom: {
factory: args => {
return (
<InfoCardPlus>
<Typography variant="h4">I'm a custom feature</Typography>
<Button
variant="contained"
color="primary"
onClick={() => {
console.log(`Here's my args 🚀`); // eslint-disable-line no-console
console.log(JSON.stringify(args, null, 2)); // eslint-disable-line no-console
}}
>
View the arguments for this feature in the console by
pressing this button
</Button>
</InfoCardPlus>
);
},
},
}}
/>
</Box>
),
})
.render();
+3 -2
View File
@@ -28,12 +28,13 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.5.3",
"@types/react": "^16.9",
"luxon": "^1.26.0",
"qs": "^6.10.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-use": "^17.2.4",
"react": "^16.13.1",
"recharts": "^1.8.5"
},
"devDependencies": {
@@ -42,8 +43,8 @@
"@backstage/dev-utils": "^0.2.2",
"@backstage/test-utils": "^0.1.14",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^3.4.2",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
@@ -18,12 +18,14 @@ import React from 'react';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { Box } from '@material-ui/core';
import { useApi } from '@backstage/core-plugin-api';
import { ContentHeader, Progress } from '@backstage/core-components';
import {
ComponentConfig,
ComponentConfigCreateRc,
ComponentConfigPatch,
ComponentConfigPromoteRc,
CreateRcOnSuccessArgs,
PatchOnSuccessArgs,
PromoteRcOnSuccessArgs,
} from './types/types';
import { Features } from './features/Features';
import { gitReleaseManagerApiRef } from './api/serviceApiRef';
@@ -33,18 +35,33 @@ import { ProjectContext, Project } from './contexts/ProjectContext';
import { RepoDetailsForm } from './features/RepoDetailsForm/RepoDetailsForm';
import { useQueryHandler } from './hooks/useQueryHandler';
import { UserContext } from './contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
import { ContentHeader, Progress } from '@backstage/core-components';
import {
GetBranchResult,
GetLatestReleaseResult,
GetRepositoryResult,
} from './api/GitReleaseClient';
interface GitReleaseManagerProps {
project?: Omit<Project, 'isProvidedViaProps'>;
features?: {
info?: Pick<ComponentConfig<void>, 'omit'>;
stats?: Pick<ComponentConfig<void>, 'omit'>;
createRc?: ComponentConfigCreateRc;
promoteRc?: ComponentConfigPromoteRc;
patch?: ComponentConfigPatch;
createRc?: ComponentConfig<CreateRcOnSuccessArgs>;
promoteRc?: ComponentConfig<PromoteRcOnSuccessArgs>;
patch?: ComponentConfig<PatchOnSuccessArgs>;
custom?: {
factory: ({
latestRelease,
project,
releaseBranch,
repository,
}: {
latestRelease: GetLatestReleaseResult['latestRelease'] | null;
project: Project;
releaseBranch: GetBranchResult['branch'] | null;
repository: GetRepositoryResult['repository'];
}) => React.ReactElement | React.ReactElement[];
};
};
}
@@ -31,7 +31,7 @@ import {
GetLatestReleaseResult,
GetRepositoryResult,
} from '../../api/GitReleaseClient';
import { ComponentConfigCreateRc } from '../../types/types';
import { ComponentConfig, CreateRcOnSuccessArgs } from '../../types/types';
import { Differ } from '../../components/Differ';
import { getReleaseCandidateGitInfo } from '../../helpers/getReleaseCandidateGitInfo';
import { InfoCardPlus } from '../../components/InfoCardPlus';
@@ -45,7 +45,7 @@ interface CreateReleaseCandidateProps {
defaultBranch: GetRepositoryResult['repository']['defaultBranch'];
latestRelease: GetLatestReleaseResult['latestRelease'];
releaseBranch: GetBranchResult['branch'] | null;
onSuccess?: ComponentConfigCreateRc['onSuccess'];
onSuccess?: ComponentConfig<CreateRcOnSuccessArgs>['onSuccess'];
}
const InfoCardPlusWrapper = ({ children }: { children: React.ReactNode }) => {
@@ -21,7 +21,11 @@ import {
GetRepositoryResult,
} from '../../../api/GitReleaseClient';
import { CardHook, ComponentConfigCreateRc } from '../../../types/types';
import {
CardHook,
ComponentConfig,
CreateRcOnSuccessArgs,
} from '../../../types/types';
import { getReleaseCandidateGitInfo } from '../../../helpers/getReleaseCandidateGitInfo';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError';
@@ -31,12 +35,12 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
import { useUserContext } from '../../../contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
interface UseCreateReleaseCandidate {
export interface UseCreateReleaseCandidate {
defaultBranch: GetRepositoryResult['repository']['defaultBranch'];
latestRelease: GetLatestReleaseResult['latestRelease'];
releaseCandidateGitInfo: ReturnType<typeof getReleaseCandidateGitInfo>;
project: Project;
onSuccess?: ComponentConfigCreateRc['onSuccess'];
onSuccess?: ComponentConfig<CreateRcOnSuccessArgs>['onSuccess'];
}
export function useCreateReleaseCandidate({
@@ -266,6 +270,12 @@ export function useCreateReleaseCandidate({
try {
await onSuccess({
input: {
defaultBranch,
latestRelease,
releaseCandidateGitInfo,
project,
},
comparisonUrl: getComparisonRes.value.htmlUrl,
createdTag: createReleaseRes.value.tagName,
gitReleaseName: createReleaseRes.value.name,
@@ -142,6 +142,14 @@ export function Features({
onSuccess={features?.patch?.onSuccess}
/>
)}
{features?.custom?.factory &&
features.custom.factory({
latestRelease: gitBatchInfo.value.latestRelease,
project,
releaseBranch: gitBatchInfo.value.releaseBranch,
repository: gitBatchInfo.value.repository,
})}
</ErrorBoundary>
</RefetchContext.Provider>
);
@@ -22,7 +22,7 @@ import {
GetBranchResult,
GetLatestReleaseResult,
} from '../../api/GitReleaseClient';
import { ComponentConfigPatch } from '../../types/types';
import { ComponentConfig, PatchOnSuccessArgs } from '../../types/types';
import { getBumpedTag } from '../../helpers/getBumpedTag';
import { InfoCardPlus } from '../../components/InfoCardPlus';
import { NoLatestRelease } from '../../components/NoLatestRelease';
@@ -32,7 +32,7 @@ import { useProjectContext } from '../../contexts/ProjectContext';
interface PatchProps {
latestRelease: GetLatestReleaseResult['latestRelease'];
releaseBranch: GetBranchResult['branch'] | null;
onSuccess?: ComponentConfigPatch['onSuccess'];
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
}
export const Patch = ({
@@ -38,7 +38,7 @@ import {
GetLatestReleaseResult,
} from '../../api/GitReleaseClient';
import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts';
import { ComponentConfigPatch } from '../../types/types';
import { ComponentConfig, PatchOnSuccessArgs } from '../../types/types';
import { Differ } from '../../components/Differ';
import { getPatchCommitSuffix } from './helpers/getPatchCommitSuffix';
import { gitReleaseManagerApiRef } from '../../api/serviceApiRef';
@@ -56,7 +56,7 @@ interface PatchBodyProps {
bumpedTag: string;
latestRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
releaseBranch: GetBranchResult['branch'];
onSuccess?: ComponentConfigPatch['onSuccess'];
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
tagParts: NonNullable<CalverTagParts | SemverTagParts>;
}
@@ -22,7 +22,11 @@ import {
} from '../../../api/GitReleaseClient';
import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts';
import { ComponentConfigPatch, CardHook } from '../../../types/types';
import {
CardHook,
ComponentConfig,
PatchOnSuccessArgs,
} from '../../../types/types';
import { getPatchCommitSuffix } from '../helpers/getPatchCommitSuffix';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
import { Project } from '../../../contexts/ProjectContext';
@@ -32,12 +36,12 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
import { useUserContext } from '../../../contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
interface Patch {
export interface UsePatch {
bumpedTag: string;
latestRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
project: Project;
tagParts: NonNullable<CalverTagParts | SemverTagParts>;
onSuccess?: ComponentConfigPatch['onSuccess'];
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
}
// Inspiration: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api
@@ -47,7 +51,7 @@ export function usePatch({
project,
tagParts,
onSuccess,
}: Patch): CardHook<GetRecentCommitsResultSingle> {
}: UsePatch): CardHook<GetRecentCommitsResultSingle> {
const pluginApiClient = useApi(gitReleaseManagerApiRef);
const { user } = useUserContext();
const {
@@ -337,13 +341,19 @@ ${selectedPatchCommit.commit.message}`,
try {
await onSuccess?.({
updatedReleaseUrl: updatedReleaseRes.value.htmlUrl,
updatedReleaseName: updatedReleaseRes.value.name,
previousTag: latestRelease.tagName,
patchedTag: updatedReleaseRes.value.tagName,
patchCommitUrl: releaseBranchRes.value.selectedPatchCommit.htmlUrl,
input: {
bumpedTag,
latestRelease,
project,
tagParts,
},
patchCommitMessage:
releaseBranchRes.value.selectedPatchCommit.commit.message,
patchCommitUrl: releaseBranchRes.value.selectedPatchCommit.htmlUrl,
patchedTag: updatedReleaseRes.value.tagName,
previousTag: latestRelease.tagName,
updatedReleaseName: updatedReleaseRes.value.name,
updatedReleaseUrl: updatedReleaseRes.value.htmlUrl,
});
} catch (error) {
asyncCatcher(error);
@@ -18,7 +18,7 @@ import React from 'react';
import { Alert, AlertTitle } from '@material-ui/lab';
import { Box, Typography } from '@material-ui/core';
import { ComponentConfigPromoteRc } from '../../types/types';
import { ComponentConfig, PromoteRcOnSuccessArgs } from '../../types/types';
import { GetLatestReleaseResult } from '../../api/GitReleaseClient';
import { InfoCardPlus } from '../../components/InfoCardPlus';
import { NoLatestRelease } from '../../components/NoLatestRelease';
@@ -27,7 +27,7 @@ import { TEST_IDS } from '../../test-helpers/test-ids';
interface PromoteRcProps {
latestRelease: GetLatestReleaseResult['latestRelease'];
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
}
export const PromoteRc = ({ latestRelease, onSuccess }: PromoteRcProps) => {
@@ -17,7 +17,7 @@
import React from 'react';
import { Button, Typography, Box } from '@material-ui/core';
import { ComponentConfigPromoteRc } from '../../types/types';
import { ComponentConfig, PromoteRcOnSuccessArgs } from '../../types/types';
import { Differ } from '../../components/Differ';
import { GetLatestReleaseResult } from '../../api/GitReleaseClient';
import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog';
@@ -26,7 +26,7 @@ import { usePromoteRc } from './hooks/usePromoteRc';
interface PromoteRcBodyProps {
rcRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
}
export const PromoteRcBody = ({ rcRelease, onSuccess }: PromoteRcBodyProps) => {
@@ -16,7 +16,11 @@
import { useState, useEffect } from 'react';
import { useAsync, useAsyncFn } from 'react-use';
import { CardHook, ComponentConfigPromoteRc } from '../../../types/types';
import {
CardHook,
ComponentConfig,
PromoteRcOnSuccessArgs,
} from '../../../types/types';
import { GetLatestReleaseResult } from '../../../api/GitReleaseClient';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
@@ -27,17 +31,17 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
import { useUserContext } from '../../../contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
interface PromoteRc {
export interface UsePromoteRc {
rcRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
releaseVersion: string;
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
}
export function usePromoteRc({
rcRelease,
releaseVersion,
onSuccess,
}: PromoteRc): CardHook<void> {
}: UsePromoteRc): CardHook<void> {
const pluginApiClient = useApi(gitReleaseManagerApiRef);
const { user } = useUserContext();
const { project } = useProjectContext();
@@ -170,12 +174,16 @@ export function usePromoteRc({
try {
await onSuccess?.({
gitReleaseUrl: promotedReleaseRes.value.htmlUrl,
input: {
rcRelease,
releaseVersion,
},
gitReleaseName: promotedReleaseRes.value.name,
previousTagUrl: rcRelease.htmlUrl,
gitReleaseUrl: promotedReleaseRes.value.htmlUrl,
previousTag: rcRelease.tagName,
updatedTagUrl: promotedReleaseRes.value.htmlUrl,
previousTagUrl: rcRelease.htmlUrl,
updatedTag: promotedReleaseRes.value.tagName,
updatedTagUrl: promotedReleaseRes.value.htmlUrl,
});
} catch (error) {
asyncCatcher(error);
+15 -11
View File
@@ -14,21 +14,26 @@
* limitations under the License.
*/
export type ComponentConfig<Args> = {
import { UseCreateReleaseCandidate } from '../features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate';
import { UsePatch } from '../features/Patch/hooks/usePatch';
import { UsePromoteRc } from '../features/PromoteRc/hooks/usePromoteRc';
export type ComponentConfig<OnSuccessArgs> = {
omit?: boolean;
onSuccess?: (args: Args) => Promise<void> | void;
onSuccess?: (args: OnSuccessArgs) => Promise<void> | void;
};
interface CreateRcOnSuccessArgs {
gitReleaseUrl: string;
gitReleaseName: string | null;
export interface CreateRcOnSuccessArgs {
input: Omit<UseCreateReleaseCandidate, 'onSuccess'>;
comparisonUrl: string;
previousTag?: string;
createdTag: string;
gitReleaseName: string | null;
gitReleaseUrl: string;
previousTag?: string;
}
export type ComponentConfigCreateRc = ComponentConfig<CreateRcOnSuccessArgs>;
interface PromoteRcOnSuccessArgs {
export interface PromoteRcOnSuccessArgs {
input: Omit<UsePromoteRc, 'onSuccess'>;
gitReleaseUrl: string;
gitReleaseName: string | null;
previousTagUrl: string;
@@ -36,9 +41,9 @@ interface PromoteRcOnSuccessArgs {
updatedTagUrl: string;
updatedTag: string;
}
export type ComponentConfigPromoteRc = ComponentConfig<PromoteRcOnSuccessArgs>;
interface PatchOnSuccessArgs {
export interface PatchOnSuccessArgs {
input: Omit<UsePatch, 'onSuccess'>;
updatedReleaseUrl: string;
updatedReleaseName: string | null;
previousTag: string;
@@ -46,7 +51,6 @@ interface PatchOnSuccessArgs {
patchCommitUrl: string;
patchCommitMessage: string;
}
export type ComponentConfigPatch = ComponentConfig<PatchOnSuccessArgs>;
export interface ResponseStep {
message: string | React.ReactNode;
+1 -1
View File
@@ -21,7 +21,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.8.5s",
"@backstage/backend-common": "^0.8.6",
"@backstage/catalog-client": "^0.3.16",
"@backstage/catalog-model": "^0.9.0",
"@backstage/config": "^0.1.5",
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
@@ -0,0 +1,162 @@
# scaffolder-backend-module-cookiecutter
Welcome to the `fetch:cookiecutter` action for the `scaffolder-backend`.
## Getting started
You need to configure the action in your backend:
## From your Backstage root directory
```
cd packages/backend
yarn add @backstage/plugin-scaffolder-backend-module-cookiecutter
```
Configure the action:
(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options):
```typescript
// packages/backend/src/plugins/scaffolder.ts
const actions = [
createFetchCookiecutterAction({
integrations,
reader,
containerRunner,
}),
...createBuiltInActions({
...
})
];
return await createRouter({
containerRunner,
logger,
config,
database,
catalogClient,
reader,
actions,
});
```
After that you can use the action in your template:
```yaml
apiVersion: backstage.io/v1beta2
kind: Template
metadata:
name: cookiecutter-demo
title: Cookiecutter Test
description: Cookiecutter example
spec:
owner: backstage/techdocs-core
type: service
parameters:
- title: Fill in some steps
required:
- name
- owner
properties:
name:
title: Name
type: string
description: Unique name of the component
ui:autofocus: true
ui:options:
rows: 5
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
system:
title: System
type: string
description: System of the component
ui:field: EntityPicker
ui:options:
allowedKinds:
- System
defaultKind: System
- title: Choose a location
required:
- repoUrl
- dryRun
properties:
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
dryRun:
title: Only perform a dry run, don't publish anything
type: boolean
default: false
steps:
- id: fetch-base
name: Fetch Base
action: fetch:cookiecutter
input:
url: ./template
values:
name: '{{ parameters.name }}'
owner: '{{ parameters.owner }}'
system: '{{ parameters.system }}'
destination: '{{ parseRepoUrl parameters.repoUrl }}'
- id: publish
if: '{{ not parameters.dryRun }}'
name: Publish
action: publish:github
input:
allowedHosts: ['github.com']
description: 'This is {{ parameters.name }}'
repoUrl: '{{ parameters.repoUrl }}'
- id: register
if: '{{ not parameters.dryRun }}'
name: Register
action: catalog:register
input:
repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}'
catalogInfoPath: '/catalog-info.yaml'
- name: Results
if: '{{ parameters.dryRun }}'
action: debug:log
input:
listWorkspace: true
output:
links:
- title: Repository
url: '{{ steps.publish.output.remoteUrl }}'
- title: Open in catalog
icon: 'catalog'
entityRef: '{{ steps.register.output.entityRef }}'
```
You can also visit the `/create/actions` route in your Backstage application to find out more about the parameters this action accepts when it's installed to configure how you like.
### Environment setup
The environment needs to have either `cookiecutter` installed and be available in the `PATH` or access to a `docker` daemon so it can spin up a docker container with `cookiecutter` available.
If you are running Backstage from a Docker container and you want to avoid calling a container inside a container, you can set up `cookiecutter` in your own image, this will use the local installation instead.
You can do so by including the following lines in the last step of your Dockerfile:
```dockerfile
RUN apt-get update && apt-get install -y python3 python3-pip
RUN pip3 install cookiecutter
```
@@ -0,0 +1,23 @@
## API Report File for "@backstage/plugin-scaffolder-backend-module-cookiecutter"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="node" />
import { ContainerRunner } from '@backstage/backend-common';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-backend';
import { UrlReader } from '@backstage/backend-common';
// Warning: (ae-missing-release-tag) "createFetchCookiecutterAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function createFetchCookiecutterAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
containerRunner: ContainerRunner;
}): TemplateAction<any>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,45 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-cookiecutter",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.8.6",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.7",
"@backstage/plugin-scaffolder-backend": "^0.14.0",
"@backstage/config": "^0.1.5",
"command-exists": "^1.2.9",
"fs-extra": "10.0.0",
"winston": "^3.2.1",
"cross-fetch": "^3.0.6",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.7.3",
"@types/fs-extra": "^9.0.1",
"@types/mock-fs": "^4.13.0",
"@types/jest": "^26.0.7",
"@types/command-exists": "^1.2.0",
"mock-fs": "^4.13.0",
"msw": "^0.29.0"
},
"files": [
"dist"
]
}
@@ -17,9 +17,12 @@ const runCommand = jest.fn();
const commandExists = jest.fn();
const fetchContents = jest.fn();
jest.mock('./helpers', () => ({ fetchContents }));
jest.mock('@backstage/plugin-scaffolder-backend', () => ({
...jest.requireActual('@backstage/plugin-scaffolder-backend'),
fetchContents,
runCommand,
}));
jest.mock('command-exists', () => commandExists);
jest.mock('../helpers', () => ({ runCommand }));
import {
getVoidLogger,
@@ -33,7 +36,7 @@ import os from 'os';
import { PassThrough } from 'stream';
import { createFetchCookiecutterAction } from './cookiecutter';
import { join } from 'path';
import { ActionContext } from '../../types';
import type { ActionContext } from '@backstage/plugin-scaffolder-backend';
describe('fetch:cookiecutter', () => {
const integrations = ScmIntegrations.fromConfig(
@@ -26,9 +26,11 @@ import commandExists from 'command-exists';
import fs from 'fs-extra';
import path, { resolve as resolvePath } from 'path';
import { Writable } from 'stream';
import { runCommand } from '../helpers';
import { createTemplateAction } from '../../createTemplateAction';
import { fetchContents } from './helpers';
import {
runCommand,
createTemplateAction,
fetchContents,
} from '@backstage/plugin-scaffolder-backend';
export class CookiecutterRunner {
private readonly containerRunner: ContainerRunner;
@@ -136,7 +138,7 @@ export function createFetchCookiecutterAction(options: {
}>({
id: 'fetch:cookiecutter',
description:
"Downloads a template from the given URL into the workspace, and runs cookiecutter on it. This action is deprecated in favor of 'fetch:template'. See https://backstage.io/docs/features/software-templates/builtin-actions#migrating-from-fetch-cookiecutter-to-fetch-template for more details.",
'Downloads a template from the given URL into the workspace, and runs cookiecutter on it.',
schema: {
input: {
type: 'object',
@@ -0,0 +1,16 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { createFetchCookiecutterAction } from './cookiecutter';
@@ -0,0 +1,16 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './fetch';
@@ -0,0 +1,16 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './actions';
+2 -8
View File
@@ -8,6 +8,7 @@
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { ContainerRunner } from '@backstage/backend-common';
import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter';
import { createPullRequest } from 'octokit-plugin-create-pull-request';
import express from 'express';
import { JsonObject } from '@backstage/config';
@@ -78,14 +79,7 @@ export function createCatalogWriteAction(): TemplateAction<any>;
// @public
export function createDebugLogAction(): TemplateAction<any>;
// Warning: (ae-missing-release-tag) "createFetchCookiecutterAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function createFetchCookiecutterAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
containerRunner: ContainerRunner;
}): TemplateAction<any>;
export { createFetchCookiecutterAction };
// Warning: (ae-missing-release-tag) "createFetchPlainAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+1
View File
@@ -35,6 +35,7 @@
"@backstage/config": "^0.1.5",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.8",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.0",
"@gitbeaker/core": "^30.2.0",
"@gitbeaker/node": "^30.2.0",
"@octokit/rest": "^18.5.3",
@@ -5,5 +5,10 @@ metadata:
description: A collection of all Backstage example templates
spec:
targets:
- ./local-templates.yaml
- ./remote-templates.yaml
# For local development of a template, you can reference your local templates here.
# Examples:
#
# - ./local-template/template.yaml
# - ../all-templates/local/template.yaml
@@ -1,13 +0,0 @@
apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
name: example-templates-local
description: A collection of locally available Backstage example templates
spec:
targets:
- ./docs-template/template.yaml
- ./react-ssr-template/template.yaml
- ./create-react-app/template.yaml
- ./springboot-grpc-template/template.yaml
- ./v1beta2-demo/template.yaml
- ./pull-request/template.yaml
@@ -7,3 +7,9 @@ spec:
type: url
targets:
- https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
- https://github.com/backstage/software-templates/blob/main/scaffolder-templates/create-react-app/template.yaml
- https://github.com/backstage/software-templates/blob/main/scaffolder-templates/docs-template/template.yaml
- https://github.com/backstage/software-templates/blob/main/scaffolder-templates/pull-request/template.yaml
- https://github.com/backstage/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml
- https://github.com/backstage/software-templates/blob/main/scaffolder-templates/springboot-grpc-template/template.yaml
- https://github.com/backstage/software-templates/blob/main/scaffolder-templates/v1beta2-demo/template.yaml
@@ -24,11 +24,8 @@ import {
} from './catalog';
import { createDebugLogAction } from './debug';
import {
createFetchCookiecutterAction,
createFetchPlainAction,
createFetchTemplateAction,
} from './fetch';
import { createFetchPlainAction, createFetchTemplateAction } from './fetch';
import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter';
import {
createFilesystemDeleteAction,
createFilesystemRenameAction,
@@ -15,6 +15,5 @@
*/
export { createFetchPlainAction } from './plain';
export { createFetchCookiecutterAction } from './cookiecutter';
export { createFetchTemplateAction } from './template';
export { fetchContents } from './helpers';
@@ -20,4 +20,6 @@ export * from './debug';
export * from './fetch';
export * from './filesystem';
export * from './publish';
export { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter';
export { runCommand } from './helpers';
+1 -1
View File
@@ -13,7 +13,7 @@ import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { IdentityApi } from '@backstage/core-plugin-api';
import { Location as Location_2 } from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/catalog-model';
import { RouteRef } from '@backstage/core-plugin-api';
// Warning: (ae-missing-release-tag) "DocsCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+11 -5
View File
@@ -17,7 +17,7 @@
import { EntityName } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { NotFoundError } from '@backstage/errors';
import { NotFoundError, ResponseError } from '@backstage/errors';
import EventSource from 'eventsource';
import { SyncResult, TechDocsApi, TechDocsStorageApi } from './api';
import { TechDocsEntityMetadata, TechDocsMetadata } from './types';
@@ -72,9 +72,12 @@ export class TechDocsClient implements TechDocsApi {
const request = await fetch(`${requestUrl}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const res = await request.json();
return res;
if (!request.ok) {
throw await ResponseError.fromResponse(request);
}
return await request.json();
}
/**
@@ -97,9 +100,12 @@ export class TechDocsClient implements TechDocsApi {
const request = await fetch(`${requestUrl}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const res = await request.json();
return res;
if (!request.ok) {
throw await ResponseError.fromResponse(request);
}
return await request.json();
}
}
@@ -19,7 +19,12 @@ import { Progress } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { BackstageTheme } from '@backstage/theme';
import { Button, CircularProgress, useTheme } from '@material-ui/core';
import {
Button,
CircularProgress,
makeStyles,
useTheme,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
@@ -45,10 +50,20 @@ type Props = {
onReady?: () => void;
};
const useStyles = makeStyles<BackstageTheme>(() => ({
message: {
// `word-break: break-word` is deprecated, but gives legacy support to browsers not supporting `overflow-wrap` yet
// https://developer.mozilla.org/en-US/docs/Web/CSS/word-break
wordBreak: 'break-word',
overflowWrap: 'anywhere',
},
}));
export const Reader = ({ entityId, onReady }: Props) => {
const { kind, namespace, name } = entityId;
const { '*': path } = useParams();
const theme = useTheme<BackstageTheme>();
const classes = useStyles();
const {
state,
@@ -369,6 +384,7 @@ export const Reader = ({ entityId, onReady }: Props) => {
variant="outlined"
severity="error"
action={<TechDocsBuildLogs buildLog={buildLog} />}
classes={{ message: classes.message }}
>
Building a newer version of this documentation failed.{' '}
{syncErrorMessage}
@@ -381,6 +397,7 @@ export const Reader = ({ entityId, onReady }: Props) => {
variant="outlined"
severity="error"
action={<TechDocsBuildLogs buildLog={buildLog} />}
classes={{ message: classes.message }}
>
Building a newer version of this documentation failed.{' '}
{syncErrorMessage}
@@ -30,7 +30,7 @@ export const TechDocsPage = () => {
const techdocsApi = useApi(techdocsApiRef);
const techdocsMetadataRequest = useAsync(() => {
const { value: techdocsMetadataValue } = useAsync(() => {
if (documentReady) {
return techdocsApi.getTechDocsMetadata({ kind, namespace, name });
}
@@ -38,7 +38,7 @@ export const TechDocsPage = () => {
return Promise.resolve(undefined);
}, [kind, namespace, name, techdocsApi, documentReady]);
const entityMetadataRequest = useAsync(() => {
const { value: entityMetadataValue } = useAsync(() => {
return techdocsApi.getEntityMetadata({ kind, namespace, name });
}, [kind, namespace, name, techdocsApi]);
@@ -49,10 +49,8 @@ export const TechDocsPage = () => {
return (
<Page themeId="documentation">
<TechDocsPageHeader
metadataRequest={{
techdocs: techdocsMetadataRequest,
entity: entityMetadataRequest,
}}
techDocsMetadata={techdocsMetadataValue}
entityMetadata={entityMetadataValue}
entityId={{
kind,
namespace,
@@ -30,26 +30,23 @@ describe('<TechDocsPageHeader />', () => {
name: 'test-name',
namespace: 'test-namespace',
}}
metadataRequest={{
entity: {
loading: false,
value: {
locationMetadata: {
type: 'github',
target: 'https://example.com/',
},
spec: {
owner: 'test',
},
},
entityMetadata={{
locationMetadata: {
type: 'github',
target: 'https://example.com/',
},
techdocs: {
loading: false,
value: {
site_name: 'test-site-name',
site_description: 'test-site-desc',
},
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'test',
},
spec: {
owner: 'test',
},
}}
techDocsMetadata={{
site_name: 'test-site-name',
site_description: 'test-site-desc',
}}
/>,
{
@@ -75,14 +72,6 @@ describe('<TechDocsPageHeader />', () => {
name: 'test-name',
namespace: 'test-namespace',
}}
metadataRequest={{
entity: {
loading: false,
},
techdocs: {
loading: false,
},
}}
/>,
{
mountedRoutes: {
@@ -105,17 +94,9 @@ describe('<TechDocsPageHeader />', () => {
name: 'test-name',
namespace: 'test-namespace',
}}
metadataRequest={{
entity: {
loading: false,
},
techdocs: {
loading: false,
value: {
site_name: 'test-site-name',
site_description: 'test-site-desc',
},
},
techDocsMetadata={{
site_name: 'test-site-name',
site_description: 'test-site-desc',
}}
/>,
{
@@ -15,6 +15,8 @@
*/
import { EntityName, RELATION_OWNED_BY } from '@backstage/catalog-model';
import { Header, HeaderLabel } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import {
EntityRefLink,
EntityRefLinks,
@@ -22,45 +24,30 @@ import {
} from '@backstage/plugin-catalog-react';
import CodeIcon from '@material-ui/icons/Code';
import React from 'react';
import { AsyncState } from 'react-use/lib/useAsync';
import { rootRouteRef } from '../../routes';
import { TechDocsMetadata } from '../../types';
import { Header, HeaderLabel } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types';
type TechDocsPageHeaderProps = {
entityId: EntityName;
metadataRequest: {
entity: AsyncState<any>;
techdocs: AsyncState<TechDocsMetadata>;
};
entityMetadata?: TechDocsEntityMetadata;
techDocsMetadata?: TechDocsMetadata;
};
export const TechDocsPageHeader = ({
entityId,
metadataRequest,
entityMetadata,
techDocsMetadata,
}: TechDocsPageHeaderProps) => {
const {
techdocs: techdocsMetadata,
entity: entityMetadata,
} = metadataRequest;
const { value: techdocsMetadataValues } = techdocsMetadata;
const { value: entityMetadataValues } = entityMetadata;
const { name } = entityId;
const { site_name: siteName, site_description: siteDescription } =
techdocsMetadataValues || {};
techDocsMetadata || {};
const {
locationMetadata,
spec: { lifecycle },
} = entityMetadataValues || { spec: {} };
const { locationMetadata, spec } = entityMetadata || {};
const lifecycle = spec?.lifecycle;
const ownedByRelations = entityMetadataValues
? getEntityRelations(entityMetadataValues, RELATION_OWNED_BY)
const ownedByRelations = entityMetadata
? getEntityRelations(entityMetadata, RELATION_OWNED_BY)
: [];
const docsRootLink = useRouteRef(rootRouteRef)();
+4 -2
View File
@@ -14,11 +14,13 @@
* limitations under the License.
*/
import { Entity, Location } from '@backstage/catalog-model';
import { Entity, LocationSpec } from '@backstage/catalog-model';
export type TechDocsMetadata = {
site_name: string;
site_description: string;
};
export type TechDocsEntityMetadata = Entity & { locationMetadata?: Location };
export type TechDocsEntityMetadata = Entity & {
locationMetadata?: LocationSpec;
};