From d2971946fc942f8ca6c2b7d8f54e7bed2364a638 Mon Sep 17 00:00:00 2001
From: ebarrios
Date: Tue, 9 Feb 2021 17:12:11 +0100
Subject: [PATCH 01/37] Add package name to lockfile.ts error
---
packages/cli/src/lib/versioning/Lockfile.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts
index f567786b7c..f0867c6408 100644
--- a/packages/cli/src/lib/versioning/Lockfile.ts
+++ b/packages/cli/src/lib/versioning/Lockfile.ts
@@ -153,7 +153,7 @@ export class Lockfile {
const acceptedVersion = versions.find(v => semver.satisfies(v, range));
if (!acceptedVersion) {
throw new Error(
- `No existing version was accepted for range ${range}, searching through ${versions}`,
+ `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`,
);
}
From 9337f509dd5c3a8b4b235db7ce83f5ec0a4c01e7 Mon Sep 17 00:00:00 2001
From: Patrik Oldsberg
Date: Tue, 9 Feb 2021 17:18:20 +0100
Subject: [PATCH 02/37] Create new-mangos-tap.md
---
.changeset/new-mangos-tap.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/new-mangos-tap.md
diff --git a/.changeset/new-mangos-tap.md b/.changeset/new-mangos-tap.md
new file mode 100644
index 0000000000..1b57510417
--- /dev/null
+++ b/.changeset/new-mangos-tap.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Tweak error message in lockfile parsing to include more information.
From 2e1f8941fe59b68b01a043d23666899529299299 Mon Sep 17 00:00:00 2001
From: Gowind
Date: Tue, 9 Feb 2021 18:18:10 +0100
Subject: [PATCH 03/37] Pass registered Logger in ServiceBuilderImpl to
requestLoggingHandler
`requestLoggingHandler` takes an optional `logger` parameter that it can use
to log incoming requests. The `ServiceBuilderImpl` was not passing on this logger, if
set, to `requestLoggingHandler` middleware
---
packages/backend-common/src/service/lib/ServiceBuilderImpl.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
index 9c5ac20fa3..07da975184 100644
--- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
+++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
@@ -161,7 +161,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
app.use(cors(corsOptions));
}
app.use(compression());
- app.use(requestLoggingHandler());
+ app.use(requestLoggingHandler(logger));
for (const [root, route] of this.routers) {
app.use(root, route);
}
From ee109df863878ef530cd162c86ec9f488be2e298 Mon Sep 17 00:00:00 2001
From: tudi2d
Date: Wed, 10 Feb 2021 23:14:02 +0100
Subject: [PATCH 04/37] Start building Breadcrumbs component
- Implement hidden breadcrumbs
---
.../Breadcrumbs/Breadcrumbs.stories.tsx | 45 +++++++++
.../layout/Breadcrumbs/Breadcrumbs.test.tsx | 15 +++
.../src/layout/Breadcrumbs/Breadcrumbs.tsx | 93 +++++++++++++++++++
packages/core/src/layout/Breadcrumbs/index.ts | 17 ++++
packages/core/src/layout/index.ts | 1 +
5 files changed, 171 insertions(+)
create mode 100644 packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
create mode 100644 packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx
create mode 100644 packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
create mode 100644 packages/core/src/layout/Breadcrumbs/index.ts
diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
new file mode 100644
index 0000000000..18569a8dad
--- /dev/null
+++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 { Breadcrumbs } from '.';
+
+export default {
+ title: 'Layout/Breadcrumbs',
+ component: Breadcrumbs,
+};
+
+const pages = [
+ {
+ href: '/',
+ name: 'A',
+ },
+ {
+ href: '/',
+ name: 'B',
+ },
+ {
+ href: '/',
+ name: 'C',
+ },
+ {
+ href: '/',
+ name: 'D',
+ },
+];
+
+// export const InHeader = () => ;
+export const OutsideOfHeader = () => ;
+// export const ExampleUsage = () => ;
diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx
new file mode 100644
index 0000000000..f3b69cc361
--- /dev/null
+++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
new file mode 100644
index 0000000000..78761d9db2
--- /dev/null
+++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
@@ -0,0 +1,93 @@
+/*
+ * 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 {
+ Link,
+ Typography,
+ Breadcrumbs as MUIBreadcrumbs,
+ Popover,
+ withStyles,
+} from '@material-ui/core';
+
+type BreadcrumbPage = {
+ href: string;
+ name: string;
+};
+
+type BreadcrumbsProps = {
+ pages: (BreadcrumbPage | BreadcrumbPage)[];
+};
+
+const UnderlinedText = withStyles({ root: { textDecoration: 'underline' } })(
+ Typography,
+);
+
+const Breadcrumb = ({ page }: { page: BreadcrumbPage }) => (
+
+ {page.name}
+
+);
+
+// Should propbably take Routes instead, to work with the react-router
+export const Breadcrumbs = ({ pages }: BreadcrumbsProps) => {
+ const [anchorEl, setAnchorEl] = React.useState(
+ null,
+ );
+ const hasHiddenBreadcrumbs = pages.length > 3;
+ const [firstPage, secondPage, ...expandablePages] = pages;
+ const currentPage = pages[pages.length - 1];
+
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const open = Boolean(anchorEl);
+
+ return (
+
+ {firstPage && pages.length > 1 && }
+ {secondPage && pages.length > 2 && }
+ {hasHiddenBreadcrumbs && (
+ ...
+ )}
+ {currentPage && {currentPage.name}}
+
+ The content of the Popover.
+
+
+ );
+};
diff --git a/packages/core/src/layout/Breadcrumbs/index.ts b/packages/core/src/layout/Breadcrumbs/index.ts
new file mode 100644
index 0000000000..6c5c2539df
--- /dev/null
+++ b/packages/core/src/layout/Breadcrumbs/index.ts
@@ -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 { Breadcrumbs } from './Breadcrumbs';
diff --git a/packages/core/src/layout/index.ts b/packages/core/src/layout/index.ts
index d97d30982e..2ab1ebaf5b 100644
--- a/packages/core/src/layout/index.ts
+++ b/packages/core/src/layout/index.ts
@@ -28,3 +28,4 @@ export * from './Page';
export * from './Sidebar';
export * from './SignInPage';
export * from './TabbedCard';
+export * from './Breadcrumbs';
From f10950bd2a87f601f35c785f2835c9b74cc41209 Mon Sep 17 00:00:00 2001
From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com>
Date: Thu, 11 Feb 2021 02:37:00 -0500
Subject: [PATCH 05/37] feat: support custom app icons
---
.changeset/eight-doors-matter.md | 19 +++++++++++++++++++
packages/app/src/App.tsx | 5 +++++
.../components/artist-lookup-component.yaml | 3 +++
packages/core-api/src/app/App.tsx | 8 ++++----
packages/core-api/src/app/types.ts | 8 ++++----
packages/core-api/src/icons/icons.tsx | 12 +++++++++---
packages/core-api/src/icons/types.ts | 6 ++++--
.../EntityLinksCard/EntityLinksCard.tsx | 7 +++----
8 files changed, 51 insertions(+), 17 deletions(-)
create mode 100644 .changeset/eight-doors-matter.md
diff --git a/.changeset/eight-doors-matter.md b/.changeset/eight-doors-matter.md
new file mode 100644
index 0000000000..7dbb56b6c3
--- /dev/null
+++ b/.changeset/eight-doors-matter.md
@@ -0,0 +1,19 @@
+---
+'@backstage/core-api': patch
+'@backstage/plugin-catalog': patch
+---
+
+Minor refactoring of BackstageApp.getSystemIcons to support custom registered
+icons. Custom Icons can be added using:
+
+```tsx
+import AlarmIcon from '@material-ui/icons/Alarm';
+import MyPersonIcon from './MyPerson';
+
+const app = createApp({
+ icons: {
+ user: MyPersonIcon // override system icon
+ alert: AlarmIcon, // Custom icon
+ },
+});
+```
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 70fceb8a01..cc673c7769 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -39,10 +39,15 @@ import { EntityPage } from './components/catalog/EntityPage';
import Root from './components/Root';
import { providers } from './identityProviders';
import * as plugins from './plugins';
+import AlarmIcon from '@material-ui/icons/Alarm';
const app = createApp({
apis,
plugins: Object.values(plugins),
+ icons: {
+ // Custom icon example
+ alert: AlarmIcon,
+ },
components: {
SignInPage: props => {
return (
diff --git a/packages/catalog-model/examples/components/artist-lookup-component.yaml b/packages/catalog-model/examples/components/artist-lookup-component.yaml
index 29c892274d..edd9b8fcf9 100644
--- a/packages/catalog-model/examples/components/artist-lookup-component.yaml
+++ b/packages/catalog-model/examples/components/artist-lookup-component.yaml
@@ -25,6 +25,9 @@ metadata:
- url: https://example.com/web
title: Website
icon: web
+ - url: https://example.com/alert
+ title: Alerts
+ icon: alert
spec:
type: service
lifecycle: experimental
diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx
index 2aaeba9796..f50bcdd015 100644
--- a/packages/core-api/src/app/App.tsx
+++ b/packages/core-api/src/app/App.tsx
@@ -48,7 +48,7 @@ import {
routeElementDiscoverer,
traverseElementTree,
} from '../extensions/traversal';
-import { IconComponent, SystemIconKey, SystemIcons } from '../icons';
+import { IconComponent, IconComponentMap, IconKey } from '../icons';
import { BackstagePlugin } from '../plugin';
import { RouteRef } from '../routing';
import {
@@ -95,7 +95,7 @@ export function generateBoundRoutes(
type FullAppOptions = {
apis: Iterable;
- icons: SystemIcons;
+ icons: IconComponentMap;
plugins: BackstagePlugin[];
components: AppComponents;
themes: AppTheme[];
@@ -144,7 +144,7 @@ export class PrivateAppImpl implements BackstageApp {
private configApi?: ConfigApi;
private readonly apis: Iterable;
- private readonly icons: SystemIcons;
+ private readonly icons: IconComponentMap;
private readonly plugins: BackstagePlugin[];
private readonly components: AppComponents;
private readonly themes: AppTheme[];
@@ -169,7 +169,7 @@ export class PrivateAppImpl implements BackstageApp {
return this.plugins;
}
- getSystemIcon(key: SystemIconKey): IconComponent {
+ getSystemIcon(key: IconKey): IconComponent {
return this.icons[key];
}
diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts
index b6b1002d12..1970868ca6 100644
--- a/packages/core-api/src/app/types.ts
+++ b/packages/core-api/src/app/types.ts
@@ -15,7 +15,7 @@
*/
import { ComponentType } from 'react';
-import { IconComponent, SystemIconKey, SystemIcons } from '../icons';
+import { IconComponent, IconComponentMap, IconKey } from '../icons';
import { BackstagePlugin, AnyExternalRoutes } from '../plugin/types';
import { RouteRef } from '../routing';
import { AnyApiFactory } from '../apis';
@@ -94,7 +94,7 @@ export type AppOptions = {
/**
* Supply icons to override the default ones.
*/
- icons?: Partial;
+ icons?: IconComponentMap;
/**
* A list of all plugins to include in the app.
@@ -169,9 +169,9 @@ export type BackstageApp = {
getPlugins(): BackstagePlugin[];
/**
- * Get a common icon for this app.
+ * Get a common or custom icon for this app.
*/
- getSystemIcon(key: SystemIconKey): IconComponent;
+ getSystemIcon(key: IconKey): IconComponent;
/**
* Provider component that should wrap the Router created with getRouter()
diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx
index 50c4b68e43..0c2b580ea7 100644
--- a/packages/core-api/src/icons/icons.tsx
+++ b/packages/core-api/src/icons/icons.tsx
@@ -15,15 +15,19 @@
*/
import { SvgIconProps } from '@material-ui/core';
+import MuiDashboardIcon from '@material-ui/icons/Dashboard';
+import MuiHelpIcon from '@material-ui/icons/Help';
import PeopleIcon from '@material-ui/icons/People';
import PersonIcon from '@material-ui/icons/Person';
import React from 'react';
import { useApp } from '../app/AppContext';
-import { IconComponent, SystemIconKey, SystemIcons } from './types';
+import { IconComponent, SystemIconKey, IconComponentMap } from './types';
-export const defaultSystemIcons: SystemIcons = {
+export const defaultSystemIcons: IconComponentMap = {
user: PersonIcon,
group: PeopleIcon,
+ dashboard: MuiDashboardIcon,
+ help: MuiHelpIcon,
};
const overridableSystemIcon = (key: SystemIconKey): IconComponent => {
@@ -35,5 +39,7 @@ const overridableSystemIcon = (key: SystemIconKey): IconComponent => {
return Component;
};
-export const UserIcon = overridableSystemIcon('user');
+export const DashboardIcon = overridableSystemIcon('dashboard');
export const GroupIcon = overridableSystemIcon('group');
+export const HelpIcon = overridableSystemIcon('help');
+export const UserIcon = overridableSystemIcon('user');
diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts
index 30e0ba53b2..be24b223ae 100644
--- a/packages/core-api/src/icons/types.ts
+++ b/packages/core-api/src/icons/types.ts
@@ -17,6 +17,8 @@
import { ComponentType } from 'react';
import { SvgIconProps } from '@material-ui/core';
+export type SystemIconKey = 'user' | 'group' | 'dashboard' | 'help';
+
export type IconComponent = ComponentType;
-export type SystemIconKey = 'user' | 'group';
-export type SystemIcons = { [key in SystemIconKey]: IconComponent };
+export type IconKey = SystemIconKey | string;
+export type IconComponentMap = { [key in IconKey]: IconComponent };
diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx
index 60fb9c3cf5..b0c3db2333 100644
--- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx
+++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
-import { IconComponent, InfoCard, useApp } from '@backstage/core';
+import { IconComponent, IconKey, InfoCard, useApp } from '@backstage/core';
import { useEntity } from '@backstage/plugin-catalog-react';
import LanguageIcon from '@material-ui/icons/Language';
import React from 'react';
@@ -33,9 +33,8 @@ export const EntityLinksCard = ({ cols = undefined }: Props) => {
const { entity } = useEntity();
const app = useApp();
- // TODO: Refactor App.icons & App.getSystemIcon to support custom icons
- const iconResolver = (key: string | undefined): IconComponent => {
- return app.getSystemIcon(key as any) ?? LanguageIcon;
+ const iconResolver = (key: IconKey | undefined): IconComponent => {
+ return app.getSystemIcon(key ?? '') ?? LanguageIcon;
};
const links = entity?.metadata?.links;
From 491f3a0ecf89119388e3c1508fc2bd02452956f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?=
Date: Mon, 8 Feb 2021 15:06:47 +0100
Subject: [PATCH 06/37] backend-common: implement UrlReader.search for the
other providers too
---
.changeset/angry-walls-mate.md | 5 +
.changeset/dry-llamas-wave.md | 7 +
.../src/reading/AzureUrlReader.test.ts | 117 ++++++++--
.../src/reading/AzureUrlReader.ts | 71 +++++--
.../src/reading/BitbucketUrlReader.test.ts | 201 ++++++++++++++++--
.../src/reading/BitbucketUrlReader.ts | 92 +++++---
.../src/reading/GithubUrlReader.test.ts | 76 ++++---
.../src/reading/GithubUrlReader.ts | 30 +--
.../src/reading/GitlabUrlReader.test.ts | 131 ++++++++++--
.../src/reading/GitlabUrlReader.ts | 79 ++++---
.../src/reading/tree/TarArchiveResponse.ts | 9 +-
.../src/reading/tree/ZipArchiveResponse.ts | 17 +-
.../backend-common/src/reading/tree/util.ts | 25 +++
packages/integration/src/ScmIntegrations.ts | 9 +-
.../src/azure/AzureIntegration.test.ts | 10 +
packages/integration/src/azure/index.ts | 3 +-
.../src/bitbucket/BitbucketIntegration.ts | 6 +-
packages/integration/src/bitbucket/index.ts | 1 +
.../src/github/GitHubIntegration.ts | 6 +-
packages/integration/src/github/index.ts | 1 +
.../src/gitlab/GitLabIntegration.ts | 6 +-
packages/integration/src/gitlab/index.ts | 1 +
packages/integration/src/helpers.test.ts | 68 +++++-
packages/integration/src/helpers.ts | 41 ++++
packages/integration/src/index.ts | 1 +
packages/integration/src/types.ts | 27 ++-
26 files changed, 822 insertions(+), 218 deletions(-)
create mode 100644 .changeset/angry-walls-mate.md
create mode 100644 .changeset/dry-llamas-wave.md
create mode 100644 packages/backend-common/src/reading/tree/util.ts
diff --git a/.changeset/angry-walls-mate.md b/.changeset/angry-walls-mate.md
new file mode 100644
index 0000000000..5902168b2c
--- /dev/null
+++ b/.changeset/angry-walls-mate.md
@@ -0,0 +1,5 @@
+---
+'@backstage/integration': minor
+---
+
+Make `ScmIntegration.resolveUrl` mandatory.
diff --git a/.changeset/dry-llamas-wave.md b/.changeset/dry-llamas-wave.md
new file mode 100644
index 0000000000..fea9b122a6
--- /dev/null
+++ b/.changeset/dry-llamas-wave.md
@@ -0,0 +1,7 @@
+---
+'@backstage/backend-common': patch
+---
+
+Implement `UrlReader.search` for the other providers (Azure, Bitbucket, GitLab) as well.
+
+The `UrlReader` subclasses now are implemented in terms of the respective `Integration` class.
diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts
index c937f8a40c..b52097baf5 100644
--- a/packages/backend-common/src/reading/AzureUrlReader.test.ts
+++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts
@@ -14,18 +14,22 @@
* limitations under the License.
*/
-import * as os from 'os';
+import { ConfigReader } from '@backstage/config';
+import {
+ AzureIntegration,
+ readAzureIntegrationConfig,
+} from '@backstage/integration';
+import { msw } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
-import path from 'path';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
-import { ConfigReader } from '@backstage/config';
+import * as os from 'os';
+import path from 'path';
+import { NotModifiedError } from '../errors';
import { getVoidLogger } from '../logging';
import { AzureUrlReader } from './AzureUrlReader';
-import { msw } from '@backstage/test-utils';
import { ReadTreeResponseFactory } from './tree';
-import { NotModifiedError } from '../errors';
const logger = getVoidLogger();
@@ -36,6 +40,16 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
describe('AzureUrlReader', () => {
+ beforeEach(() => {
+ mockFs({
+ [tmpDir]: mockFs.directory(),
+ });
+ });
+
+ afterEach(() => {
+ mockFs.restore();
+ });
+
const worker = setupServer();
msw.setupDefaultHandlers(worker);
@@ -143,22 +157,18 @@ describe('AzureUrlReader', () => {
});
describe('readTree', () => {
- beforeEach(() => {
- mockFs({
- [tmpDir]: mockFs.directory(),
- });
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
const repoBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'),
);
const processor = new AzureUrlReader(
- { host: 'dev.azure.com' },
+ new AzureIntegration(
+ readAzureIntegrationConfig(
+ new ConfigReader({
+ host: 'dev.azure.com',
+ }),
+ ),
+ ),
{ treeResponseFactory },
);
@@ -257,4 +267,79 @@ describe('AzureUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
});
+
+ describe('search', () => {
+ const repoBuffer = fs.readFileSync(
+ path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'),
+ );
+
+ const processor = new AzureUrlReader(
+ new AzureIntegration(
+ readAzureIntegrationConfig(
+ new ConfigReader({
+ host: 'dev.azure.com',
+ }),
+ ),
+ ),
+ { treeResponseFactory },
+ );
+
+ beforeEach(() => {
+ worker.use(
+ rest.get(
+ 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/zip'),
+ ctx.body(repoBuffer),
+ ),
+ ),
+ rest.get(
+ // https://docs.microsoft.com/en-us/rest/api/azure/devops/git/commits/get%20commits?view=azure-devops-rest-6.0#on-a-branch
+ 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/commits',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.json({
+ count: 2,
+ value: [
+ {
+ commitId: '123abc2',
+ comment: 'second commit',
+ },
+ {
+ commitId: '123abc1',
+ comment: 'first commit',
+ },
+ ],
+ }),
+ ),
+ ),
+ );
+ });
+
+ it('works for the naive case', async () => {
+ const result = await processor.search(
+ 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=%2F**%2Findex.*&version=GBmaster',
+ );
+ expect(result.etag).toBe('123abc2');
+ expect(result.files.length).toBe(1);
+ expect(result.files[0].url).toBe(
+ 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=%2Fdocs%2Findex.md&version=GBmaster',
+ );
+ await expect(result.files[0].content()).resolves.toEqual(
+ Buffer.from('# Test\n'),
+ );
+ });
+
+ it('throws NotModifiedError when same etag', async () => {
+ await expect(
+ processor.search(
+ 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=**/index.*&version=GBmaster',
+ { etag: '123abc2' },
+ ),
+ ).rejects.toThrow(NotModifiedError);
+ });
+ });
});
diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts
index bc1b61a041..21b988e5b0 100644
--- a/packages/backend-common/src/reading/AzureUrlReader.ts
+++ b/packages/backend-common/src/reading/AzureUrlReader.ts
@@ -15,39 +15,41 @@
*/
import {
- AzureIntegrationConfig,
- readAzureIntegrationConfigs,
- getAzureFileFetchUrl,
- getAzureDownloadUrl,
- getAzureRequestOptions,
+ AzureIntegration,
getAzureCommitsUrl,
+ getAzureDownloadUrl,
+ getAzureFileFetchUrl,
+ getAzureRequestOptions,
+ ScmIntegrations,
} from '@backstage/integration';
import fetch from 'cross-fetch';
+import parseGitUrl from 'git-url-parse';
+import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '../errors';
+import { ReadTreeResponseFactory } from './tree';
+import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
+ SearchOptions,
SearchResponse,
UrlReader,
} from './types';
-import { ReadTreeResponseFactory } from './tree';
export class AzureUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
- const configs = readAzureIntegrationConfigs(
- config.getOptionalConfigArray('integrations.azure') ?? [],
- );
- return configs.map(options => {
- const reader = new AzureUrlReader(options, { treeResponseFactory });
- const predicate = (url: URL) => url.host === options.host;
+ const integrations = ScmIntegrations.fromConfig(config);
+ return integrations.azure.list().map(integration => {
+ const reader = new AzureUrlReader(integration, { treeResponseFactory });
+ const predicate = (url: URL) => url.host === integration.config.host;
return { reader, predicate };
});
};
constructor(
- private readonly options: AzureIntegrationConfig,
+ private readonly integration: AzureIntegration,
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
) {}
@@ -56,7 +58,10 @@ export class AzureUrlReader implements UrlReader {
let response: Response;
try {
- response = await fetch(builtUrl, getAzureRequestOptions(this.options));
+ response = await fetch(
+ builtUrl,
+ getAzureRequestOptions(this.integration.config),
+ );
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
}
@@ -83,7 +88,7 @@ export class AzureUrlReader implements UrlReader {
const commitsAzureResponse = await fetch(
getAzureCommitsUrl(url),
- getAzureRequestOptions(this.options),
+ getAzureRequestOptions(this.integration.config),
);
if (!commitsAzureResponse.ok) {
const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`;
@@ -100,7 +105,9 @@ export class AzureUrlReader implements UrlReader {
const archiveAzureResponse = await fetch(
getAzureDownloadUrl(url),
- getAzureRequestOptions(this.options, { Accept: 'application/zip' }),
+ getAzureRequestOptions(this.integration.config, {
+ Accept: 'application/zip',
+ }),
);
if (!archiveAzureResponse.ok) {
const message = `Failed to read tree from ${url}, ${archiveAzureResponse.status} ${archiveAzureResponse.statusText}`;
@@ -117,12 +124,38 @@ export class AzureUrlReader implements UrlReader {
});
}
- async search(): Promise {
- throw new Error('AzureUrlReader does not implement search');
+ async search(url: string, options?: SearchOptions): Promise {
+ const { filepath } = parseGitUrl(url);
+ const matcher = new Minimatch(filepath);
+
+ // TODO(freben): For now, read the entire repo and filter through that. In
+ // a future improvement, we could be smart and try to deduce that non-glob
+ // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
+ // to get just that part of the repo.
+ const treeUrl = new URL(url);
+ treeUrl.searchParams.delete('path');
+ treeUrl.pathname = treeUrl.pathname.replace(/\/+$/, '');
+
+ const tree = await this.readTree(treeUrl.toString(), {
+ etag: options?.etag,
+ filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
+ });
+ const files = await tree.files();
+
+ return {
+ etag: tree.etag,
+ files: files.map(file => ({
+ url: this.integration.resolveUrl({
+ url: `/${file.path}`,
+ base: url,
+ }),
+ content: file.content,
+ })),
+ };
}
toString() {
- const { host, token } = this.options;
+ const { host, token } = this.integration.config;
return `azure{host=${host},authed=${Boolean(token)}}`;
}
}
diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
index 974c84b2c2..df9febb352 100644
--- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
+++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
@@ -15,11 +15,16 @@
*/
import { ConfigReader } from '@backstage/config';
+import {
+ BitbucketIntegration,
+ readBitbucketIntegrationConfig,
+} from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
+import os from 'os';
import path from 'path';
import { NotModifiedError } from '../errors';
import { BitbucketUrlReader } from './BitbucketUrlReader';
@@ -30,19 +35,45 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
});
const bitbucketProcessor = new BitbucketUrlReader(
- { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' },
+ new BitbucketIntegration(
+ readBitbucketIntegrationConfig(
+ new ConfigReader({
+ host: 'bitbucket.org',
+ apiBaseUrl: 'https://api.bitbucket.org/2.0',
+ }),
+ ),
+ ),
{ treeResponseFactory },
);
const hostedBitbucketProcessor = new BitbucketUrlReader(
- {
- host: 'bitbucket.mycompany.net',
- apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
- },
+ new BitbucketIntegration(
+ readBitbucketIntegrationConfig(
+ new ConfigReader({
+ host: 'bitbucket.mycompany.net',
+ apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
+ }),
+ ),
+ ),
{ treeResponseFactory },
);
+const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
+
describe('BitbucketUrlReader', () => {
+ beforeEach(() => {
+ mockFs({
+ [tmpDir]: mockFs.directory(),
+ });
+ });
+
+ afterEach(() => {
+ mockFs.restore();
+ });
+
+ const worker = setupServer();
+ msw.setupDefaultHandlers(worker);
+
describe('implementation', () => {
it('rejects unknown targets', async () => {
await expect(
@@ -54,19 +85,6 @@ describe('BitbucketUrlReader', () => {
});
describe('readTree', () => {
- beforeEach(() => {
- mockFs({
- '/tmp': mockFs.directory(),
- });
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
- const worker = setupServer();
- msw.setupDefaultHandlers(worker);
-
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
@@ -247,12 +265,153 @@ describe('BitbucketUrlReader', () => {
expect(() => {
/* eslint-disable no-new */
new BitbucketUrlReader(
- {
- host: 'bitbucket.mycompany.net',
- },
+ new BitbucketIntegration(
+ readBitbucketIntegrationConfig(
+ new ConfigReader({
+ host: 'bitbucket.mycompany.net',
+ }),
+ ),
+ ),
{ treeResponseFactory },
);
}).toThrowError('must configure an explicit apiBaseUrl');
});
});
+
+ describe('search hosted', () => {
+ const repoBuffer = fs.readFileSync(
+ path.resolve(
+ 'src',
+ 'reading',
+ '__fixtures__',
+ 'bitbucket-repo-with-commit-hash.zip',
+ ),
+ );
+
+ beforeEach(() => {
+ worker.use(
+ rest.get(
+ 'https://api.bitbucket.org/2.0/repositories/backstage/mock',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.json({
+ mainbranch: {
+ type: 'branch',
+ name: 'master',
+ },
+ }),
+ ),
+ ),
+ rest.get(
+ 'https://bitbucket.org/backstage/mock/get/master.zip',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/zip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=backstage-mock-12ab34cd56ef.zip',
+ ),
+ ctx.body(repoBuffer),
+ ),
+ ),
+ rest.get(
+ 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.json({
+ values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
+ }),
+ ),
+ ),
+ );
+ });
+
+ it('works for the naive case', async () => {
+ const result = await bitbucketProcessor.search(
+ 'https://bitbucket.org/backstage/mock/src/master/**/index.*',
+ );
+ expect(result.etag).toBe('12ab34cd56ef');
+ expect(result.files.length).toBe(1);
+ expect(result.files[0].url).toBe(
+ 'https://bitbucket.org/backstage/mock/src/master/docs/index.md',
+ );
+ await expect(result.files[0].content()).resolves.toEqual(
+ Buffer.from('# Test\n'),
+ );
+ });
+
+ it('throws NotModifiedError when same etag', async () => {
+ await expect(
+ bitbucketProcessor.search(
+ 'https://bitbucket.org/backstage/mock/src/master/**/index.*',
+ { etag: '12ab34cd56ef' },
+ ),
+ ).rejects.toThrow(NotModifiedError);
+ });
+ });
+
+ describe('search private', () => {
+ const privateBitbucketRepoBuffer = fs.readFileSync(
+ path.resolve(
+ 'src',
+ 'reading',
+ '__fixtures__',
+ 'bitbucket-server-repo.zip',
+ ),
+ );
+
+ beforeEach(() => {
+ worker.use(
+ rest.get(
+ 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&prefix=mock&path=docs',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/zip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=backstage-mock.zip',
+ ),
+ ctx.body(privateBitbucketRepoBuffer),
+ ),
+ ),
+ rest.get(
+ 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.json({
+ values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
+ }),
+ ),
+ ),
+ );
+ });
+
+ it('works for the naive case', async () => {
+ const result = await hostedBitbucketProcessor.search(
+ 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master',
+ );
+ expect(result.etag).toBe('12ab34cd56ef');
+ expect(result.files.length).toBe(1);
+ expect(result.files[0].url).toBe(
+ 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
+ );
+ await expect(result.files[0].content()).resolves.toEqual(
+ Buffer.from('# Test\n'),
+ );
+ });
+
+ it('throws NotModifiedError when same etag', async () => {
+ await expect(
+ hostedBitbucketProcessor.search(
+ 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master',
+ { etag: '12ab34cd56ef' },
+ ),
+ ).rejects.toThrow(NotModifiedError);
+ });
+ });
});
diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts
index e51c03e562..3954e7ee4a 100644
--- a/packages/backend-common/src/reading/BitbucketUrlReader.ts
+++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts
@@ -15,22 +15,25 @@
*/
import {
- BitbucketIntegrationConfig,
+ BitbucketIntegration,
getBitbucketDefaultBranch,
getBitbucketDownloadUrl,
getBitbucketFileFetchUrl,
getBitbucketRequestOptions,
- readBitbucketIntegrationConfigs,
+ ScmIntegrations,
} from '@backstage/integration';
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
+import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '../errors';
import { ReadTreeResponseFactory } from './tree';
+import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
+ SearchOptions,
SearchResponse,
UrlReader,
} from './types';
@@ -40,45 +43,43 @@ import {
* the one exposed by Bitbucket Cloud itself.
*/
export class BitbucketUrlReader implements UrlReader {
- private readonly config: BitbucketIntegrationConfig;
- private readonly treeResponseFactory: ReadTreeResponseFactory;
-
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
- const configs = readBitbucketIntegrationConfigs(
- config.getOptionalConfigArray('integrations.bitbucket') ?? [],
- );
- return configs.map(provider => {
- const reader = new BitbucketUrlReader(provider, { treeResponseFactory });
- const predicate = (url: URL) => url.host === provider.host;
+ const integrations = ScmIntegrations.fromConfig(config);
+ return integrations.bitbucket.list().map(integration => {
+ const reader = new BitbucketUrlReader(integration, {
+ treeResponseFactory,
+ });
+ const predicate = (url: URL) => url.host === integration.config.host;
return { reader, predicate };
});
};
constructor(
- config: BitbucketIntegrationConfig,
- deps: { treeResponseFactory: ReadTreeResponseFactory },
+ private readonly integration: BitbucketIntegration,
+ private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
) {
- const { host, apiBaseUrl, token, username, appPassword } = config;
+ const {
+ host,
+ apiBaseUrl,
+ token,
+ username,
+ appPassword,
+ } = integration.config;
if (!apiBaseUrl) {
throw new Error(
`Bitbucket integration for '${host}' must configure an explicit apiBaseUrl`,
);
- }
-
- if (!token && username && !appPassword) {
+ } else if (!token && username && !appPassword) {
throw new Error(
`Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`,
);
}
-
- this.config = config;
- this.treeResponseFactory = deps.treeResponseFactory;
}
async read(url: string): Promise {
- const bitbucketUrl = getBitbucketFileFetchUrl(url, this.config);
- const options = getBitbucketRequestOptions(this.config);
+ const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config);
+ const options = getBitbucketRequestOptions(this.integration.config);
let response: Response;
try {
@@ -109,10 +110,13 @@ export class BitbucketUrlReader implements UrlReader {
throw new NotModifiedError();
}
- const downloadUrl = await getBitbucketDownloadUrl(url, this.config);
+ const downloadUrl = await getBitbucketDownloadUrl(
+ url,
+ this.integration.config,
+ );
const archiveBitbucketResponse = await fetch(
downloadUrl,
- getBitbucketRequestOptions(this.config),
+ getBitbucketRequestOptions(this.integration.config),
);
if (!archiveBitbucketResponse.ok) {
const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`;
@@ -122,7 +126,7 @@ export class BitbucketUrlReader implements UrlReader {
throw new Error(message);
}
- return await this.treeResponseFactory.fromZipArchive({
+ return await this.deps.treeResponseFactory.fromZipArchive({
stream: (archiveBitbucketResponse.body as unknown) as Readable,
subpath: filepath,
etag: lastCommitShortHash,
@@ -130,12 +134,36 @@ export class BitbucketUrlReader implements UrlReader {
});
}
- async search(): Promise {
- throw new Error('BitbucketUrlReader does not implement search');
+ async search(url: string, options?: SearchOptions): Promise {
+ const { filepath } = parseGitUrl(url);
+ const matcher = new Minimatch(filepath);
+
+ // TODO(freben): For now, read the entire repo and filter through that. In
+ // a future improvement, we could be smart and try to deduce that non-glob
+ // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
+ // to get just that part of the repo.
+ const treeUrl = url.replace(filepath, '').replace(/\/+$/, '');
+
+ const tree = await this.readTree(treeUrl, {
+ etag: options?.etag,
+ filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
+ });
+ const files = await tree.files();
+
+ return {
+ etag: tree.etag,
+ files: files.map(file => ({
+ url: this.integration.resolveUrl({
+ url: `/${file.path}`,
+ base: url,
+ }),
+ content: file.content,
+ })),
+ };
}
toString() {
- const { host, token, username, appPassword } = this.config;
+ const { host, token, username, appPassword } = this.integration.config;
let authed = Boolean(token);
if (!authed) {
authed = Boolean(username && appPassword);
@@ -148,18 +176,18 @@ export class BitbucketUrlReader implements UrlReader {
let branch = ref;
if (!branch) {
- branch = await getBitbucketDefaultBranch(url, this.config);
+ branch = await getBitbucketDefaultBranch(url, this.integration.config);
}
const isHosted = resource === 'bitbucket.org';
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222
const commitsApiUrl = isHosted
- ? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
- : `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
+ ? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
+ : `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
const commitsResponse = await fetch(
commitsApiUrl,
- getBitbucketRequestOptions(this.config),
+ getBitbucketRequestOptions(this.integration.config),
);
if (!commitsResponse.ok) {
const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`;
diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts
index d4e579b143..72fbf83f6d 100644
--- a/packages/backend-common/src/reading/GithubUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts
@@ -15,12 +15,17 @@
*/
import { ConfigReader } from '@backstage/config';
-import { GithubCredentialsProvider } from '@backstage/integration';
+import {
+ GithubCredentialsProvider,
+ GitHubIntegration,
+ readGitHubIntegrationConfig,
+} from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
+import os from 'os';
import path from 'path';
import { NotFoundError, NotModifiedError } from '../errors';
import {
@@ -41,26 +46,45 @@ const mockCredentialsProvider = ({
} as unknown) as GithubCredentialsProvider;
const githubProcessor = new GithubUrlReader(
- {
- host: 'github.com',
- apiBaseUrl: 'https://api.github.com',
- },
+ new GitHubIntegration(
+ readGitHubIntegrationConfig(
+ new ConfigReader({
+ host: 'github.com',
+ apiBaseUrl: 'https://api.github.com',
+ }),
+ ),
+ ),
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
const gheProcessor = new GithubUrlReader(
- {
- host: 'ghe.github.com',
- apiBaseUrl: 'https://ghe.github.com/api/v3',
- },
+ new GitHubIntegration(
+ readGitHubIntegrationConfig(
+ new ConfigReader({
+ host: 'ghe.github.com',
+ apiBaseUrl: 'https://ghe.github.com/api/v3',
+ }),
+ ),
+ ),
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
+const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
+
describe('GithubUrlReader', () => {
const worker = setupServer();
-
msw.setupDefaultHandlers(worker);
+ beforeEach(() => {
+ mockFs({
+ [tmpDir]: mockFs.directory(),
+ });
+ });
+
+ afterEach(() => {
+ mockFs.restore();
+ });
+
beforeEach(() => {
jest.clearAllMocks();
});
@@ -94,7 +118,7 @@ describe('GithubUrlReader', () => {
worker.use(
rest.get(
- 'https://api.github.com/repos/backstage/mock/tree/contents/?ref=main',
+ 'https://ghe.github.com/api/v3/repos/backstage/mock/tree/contents/?ref=main',
(req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(
mockHeaders.Authorization,
@@ -111,7 +135,7 @@ describe('GithubUrlReader', () => {
),
);
- await githubProcessor.read(
+ await gheProcessor.read(
'https://github.com/backstage/mock/tree/blob/main',
);
});
@@ -122,16 +146,6 @@ describe('GithubUrlReader', () => {
*/
describe('readTree', () => {
- beforeEach(() => {
- mockFs({
- '/tmp': mockFs.directory(),
- });
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
@@ -397,9 +411,13 @@ describe('GithubUrlReader', () => {
expect(() => {
/* eslint-disable no-new */
new GithubUrlReader(
- {
- host: 'ghe.mycompany.net',
- },
+ new GitHubIntegration(
+ readGitHubIntegrationConfig(
+ new ConfigReader({
+ host: 'ghe.mycompany.net',
+ }),
+ ),
+ ),
{
treeResponseFactory,
credentialsProvider: mockCredentialsProvider,
@@ -414,14 +432,6 @@ describe('GithubUrlReader', () => {
*/
describe('search', () => {
- beforeEach(() => {
- mockFs({ '/tmp': mockFs.directory() });
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts
index 291740a9fc..ac5a93bd5f 100644
--- a/packages/backend-common/src/reading/GithubUrlReader.ts
+++ b/packages/backend-common/src/reading/GithubUrlReader.ts
@@ -17,8 +17,8 @@
import {
getGitHubFileFetchUrl,
GithubCredentialsProvider,
- GitHubIntegrationConfig,
- readGitHubIntegrationConfigs,
+ GitHubIntegration,
+ ScmIntegrations,
} from '@backstage/integration';
import { RestEndpointMethodTypes } from '@octokit/rest';
import fetch from 'cross-fetch';
@@ -48,36 +48,36 @@ export type GhBlobResponse = RestEndpointMethodTypes['git']['getBlob']['response
*/
export class GithubUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
- const configs = readGitHubIntegrationConfigs(
- config.getOptionalConfigArray('integrations.github') ?? [],
- );
- return configs.map(provider => {
- const credentialsProvider = GithubCredentialsProvider.create(provider);
- const reader = new GithubUrlReader(provider, {
+ const integrations = ScmIntegrations.fromConfig(config);
+ return integrations.github.list().map(integration => {
+ const credentialsProvider = GithubCredentialsProvider.create(
+ integration.config,
+ );
+ const reader = new GithubUrlReader(integration, {
treeResponseFactory,
credentialsProvider,
});
- const predicate = (url: URL) => url.host === provider.host;
+ const predicate = (url: URL) => url.host === integration.config.host;
return { reader, predicate };
});
};
constructor(
- private readonly config: GitHubIntegrationConfig,
+ private readonly integration: GitHubIntegration,
private readonly deps: {
treeResponseFactory: ReadTreeResponseFactory;
credentialsProvider: GithubCredentialsProvider;
},
) {
- if (!config.apiBaseUrl && !config.rawBaseUrl) {
+ if (!integration.config.apiBaseUrl && !integration.config.rawBaseUrl) {
throw new Error(
- `GitHub integration for '${config.host}' must configure an explicit apiBaseUrl and rawBaseUrl`,
+ `GitHub integration '${integration.title}' must configure an explicit apiBaseUrl or rawBaseUrl`,
);
}
}
async read(url: string): Promise {
- const ghUrl = getGitHubFileFetchUrl(url, this.config);
+ const ghUrl = getGitHubFileFetchUrl(url, this.integration.config);
const { headers } = await this.deps.credentialsProvider.getCredentials({
url,
});
@@ -155,7 +155,7 @@ export class GithubUrlReader implements UrlReader {
}
toString() {
- const { host, token } = this.config;
+ const { host, token } = this.integration.config;
return `github{host=${host},authed=${Boolean(token)}}`;
}
@@ -258,7 +258,7 @@ export class GithubUrlReader implements UrlReader {
});
const repo: GhRepoResponse = await this.fetchJson(
- `${this.config.apiBaseUrl}/repos/${full_name}`,
+ `${this.integration.config.apiBaseUrl}/repos/${full_name}`,
{ headers },
);
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
index 5b0b4002a3..636540caa8 100644
--- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
@@ -20,11 +20,16 @@ import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
+import os from 'os';
import path from 'path';
import { getVoidLogger } from '../logging';
import { GitlabUrlReader } from './GitlabUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { NotModifiedError, NotFoundError } from '../errors';
+import {
+ GitLabIntegration,
+ readGitLabIntegrationConfig,
+} from '@backstage/integration';
const logger = getVoidLogger();
@@ -33,24 +38,44 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
});
const gitlabProcessor = new GitlabUrlReader(
- {
- host: 'gitlab.com',
- apiBaseUrl: 'https://gitlab.com/api/v4',
- baseUrl: 'https://gitlab.com',
- },
+ new GitLabIntegration(
+ readGitLabIntegrationConfig(
+ new ConfigReader({
+ host: 'gitlab.com',
+ apiBaseUrl: 'https://gitlab.com/api/v4',
+ baseUrl: 'https://gitlab.com',
+ }),
+ ),
+ ),
{ treeResponseFactory },
);
const hostedGitlabProcessor = new GitlabUrlReader(
- {
- host: 'gitlab.mycompany.com',
- apiBaseUrl: 'https://gitlab.mycompany.com/api/v4',
- baseUrl: 'https://gitlab.mycompany.com',
- },
+ new GitLabIntegration(
+ readGitLabIntegrationConfig(
+ new ConfigReader({
+ host: 'gitlab.mycompany.com',
+ apiBaseUrl: 'https://gitlab.mycompany.com/api/v4',
+ baseUrl: 'https://gitlab.mycompany.com',
+ }),
+ ),
+ ),
{ treeResponseFactory },
);
+const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
+
describe('GitlabUrlReader', () => {
+ beforeEach(() => {
+ mockFs({
+ [tmpDir]: mockFs.directory(),
+ });
+ });
+
+ afterEach(() => {
+ mockFs.restore();
+ });
+
const worker = setupServer();
msw.setupDefaultHandlers(worker);
@@ -156,16 +181,6 @@ describe('GitlabUrlReader', () => {
});
describe('readTree', () => {
- beforeEach(() => {
- mockFs({
- '/tmp': mockFs.directory(),
- });
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
const archiveBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'),
);
@@ -382,4 +397,80 @@ describe('GitlabUrlReader', () => {
await expect(fnGithub).rejects.toThrow(NotFoundError);
});
});
+
+ describe('search', () => {
+ const archiveBuffer = fs.readFileSync(
+ path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'),
+ );
+
+ const projectGitlabApiResponse = {
+ id: 11111111,
+ default_branch: 'main',
+ };
+
+ const branchGitlabApiResponse = {
+ commit: {
+ id: 'sha123abc',
+ },
+ };
+
+ beforeEach(() => {
+ worker.use(
+ rest.get(
+ 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/archive.zip?sha=main',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/zip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename="mock-main-sha123abc.zip"',
+ ),
+ ctx.body(archiveBuffer),
+ ),
+ ),
+ rest.get(
+ 'https://gitlab.com/api/v4/projects/backstage%2Fmock',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/json'),
+ ctx.json(projectGitlabApiResponse),
+ ),
+ ),
+ rest.get(
+ 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/main',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/json'),
+ ctx.json(branchGitlabApiResponse),
+ ),
+ ),
+ );
+ });
+
+ it('works for the naive case', async () => {
+ const result = await gitlabProcessor.search(
+ 'https://gitlab.com/backstage/mock/tree/main/**/index.*',
+ );
+ expect(result.etag).toBe('sha123abc');
+ expect(result.files.length).toBe(1);
+ expect(result.files[0].url).toBe(
+ 'https://gitlab.com/backstage/mock/tree/main/docs/index.md',
+ );
+ await expect(result.files[0].content()).resolves.toEqual(
+ Buffer.from('# Test\n'),
+ );
+ });
+
+ it('throws NotModifiedError when same etag', async () => {
+ await expect(
+ gitlabProcessor.search(
+ 'https://gitlab.com/backstage/mock/tree/main/**/index.*',
+ { etag: 'sha123abc' },
+ ),
+ ).rejects.toThrow(NotModifiedError);
+ });
+ });
});
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts
index 66030e30d9..66ceae05c6 100644
--- a/packages/backend-common/src/reading/GitlabUrlReader.ts
+++ b/packages/backend-common/src/reading/GitlabUrlReader.ts
@@ -17,49 +17,51 @@
import {
getGitLabFileFetchUrl,
getGitLabRequestOptions,
- GitLabIntegrationConfig,
- readGitLabIntegrationConfigs,
+ GitLabIntegration,
+ ScmIntegrations,
} from '@backstage/integration';
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
+import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '../errors';
import { ReadTreeResponseFactory } from './tree';
+import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
+ SearchOptions,
SearchResponse,
UrlReader,
} from './types';
export class GitlabUrlReader implements UrlReader {
- private readonly treeResponseFactory: ReadTreeResponseFactory;
-
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
- const configs = readGitLabIntegrationConfigs(
- config.getOptionalConfigArray('integrations.gitlab') ?? [],
- );
- return configs.map(provider => {
- const reader = new GitlabUrlReader(provider, { treeResponseFactory });
- const predicate = (url: URL) => url.host === provider.host;
+ const integrations = ScmIntegrations.fromConfig(config);
+ return integrations.gitlab.list().map(integration => {
+ const reader = new GitlabUrlReader(integration, {
+ treeResponseFactory,
+ });
+ const predicate = (url: URL) => url.host === integration.config.host;
return { reader, predicate };
});
};
constructor(
- private readonly config: GitLabIntegrationConfig,
- deps: { treeResponseFactory: ReadTreeResponseFactory },
- ) {
- this.treeResponseFactory = deps.treeResponseFactory;
- }
+ private readonly integration: GitLabIntegration,
+ private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
+ ) {}
async read(url: string): Promise {
- const builtUrl = await getGitLabFileFetchUrl(url, this.config);
+ const builtUrl = await getGitLabFileFetchUrl(url, this.integration.config);
let response: Response;
try {
- response = await fetch(builtUrl, getGitLabRequestOptions(this.config));
+ response = await fetch(
+ builtUrl,
+ getGitLabRequestOptions(this.integration.config),
+ );
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
}
@@ -86,9 +88,11 @@ export class GitlabUrlReader implements UrlReader {
// https://docs.gitlab.com/ee/api/README.html#namespaced-path-encoding
const projectGitlabResponse = await fetch(
new URL(
- `${this.config.apiBaseUrl}/projects/${encodeURIComponent(full_name)}`,
+ `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent(
+ full_name,
+ )}`,
).toString(),
- getGitLabRequestOptions(this.config),
+ getGitLabRequestOptions(this.integration.config),
);
if (!projectGitlabResponse.ok) {
const msg = `Failed to read tree from ${url}, ${projectGitlabResponse.status} ${projectGitlabResponse.statusText}`;
@@ -106,11 +110,11 @@ export class GitlabUrlReader implements UrlReader {
// the provided sha.
const branchGitlabResponse = await fetch(
new URL(
- `${this.config.apiBaseUrl}/projects/${encodeURIComponent(
+ `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent(
full_name,
)}/repository/branches/${branch}`,
).toString(),
- getGitLabRequestOptions(this.config),
+ getGitLabRequestOptions(this.integration.config),
);
if (!branchGitlabResponse.ok) {
const message = `Failed to read tree (branch) from ${url}, ${branchGitlabResponse.status} ${branchGitlabResponse.statusText}`;
@@ -128,10 +132,10 @@ export class GitlabUrlReader implements UrlReader {
// https://docs.gitlab.com/ee/api/repositories.html#get-file-archive
const archiveGitLabResponse = await fetch(
- `${this.config.apiBaseUrl}/projects/${encodeURIComponent(
+ `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent(
full_name,
)}/repository/archive.zip?sha=${branch}`,
- getGitLabRequestOptions(this.config),
+ getGitLabRequestOptions(this.integration.config),
);
if (!archiveGitLabResponse.ok) {
const message = `Failed to read tree (archive) from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`;
@@ -141,7 +145,7 @@ export class GitlabUrlReader implements UrlReader {
throw new Error(message);
}
- return await this.treeResponseFactory.fromZipArchive({
+ return await this.deps.treeResponseFactory.fromZipArchive({
stream: (archiveGitLabResponse.body as unknown) as Readable,
subpath: filepath,
etag: commitSha,
@@ -149,12 +153,33 @@ export class GitlabUrlReader implements UrlReader {
});
}
- async search(): Promise {
- throw new Error('GitlabUrlReader does not implement search');
+ async search(url: string, options?: SearchOptions): Promise {
+ const { filepath } = parseGitUrl(url);
+ const matcher = new Minimatch(filepath);
+
+ // TODO(freben): For now, read the entire repo and filter through that. In
+ // a future improvement, we could be smart and try to deduce that non-glob
+ // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
+ // to get just that part of the repo.
+ const treeUrl = url.replace(filepath, '').replace(/\/+$/, '');
+
+ const tree = await this.readTree(treeUrl, {
+ etag: options?.etag,
+ filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
+ });
+ const files = await tree.files();
+
+ return {
+ etag: tree.etag,
+ files: files.map(file => ({
+ url: this.integration.resolveUrl({ url: `/${file.path}`, base: url }),
+ content: file.content,
+ })),
+ };
}
toString() {
- const { host, token } = this.config;
+ const { host, token } = this.integration.config;
return `gitlab{host=${host},authed=${Boolean(token)}}`;
}
}
diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
index d22529293f..e61a1df645 100644
--- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
+++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
@@ -25,15 +25,12 @@ import {
ReadTreeResponseDirOptions,
ReadTreeResponseFile,
} from '../types';
+import { stripFirstDirectoryFromPath } from './util';
// Tar types for `Parse` is not a proper constructor, but it should be
const TarParseStream = (Parse as unknown) as { new (): ParseStream };
const pipeline = promisify(pipelineCb);
-// Matches a directory name + one `/` at the start of any string,
-// containing any character except `/` one or more times, and ending with a `/`
-// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
-const directoryNameRegex = /^[^\/]+\//;
/**
* Wraps a tar archive stream into a tree response reader.
@@ -84,7 +81,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
- const relativePath = entry.path.replace(directoryNameRegex, '');
+ const relativePath = stripFirstDirectoryFromPath(entry.path);
if (this.subPath) {
if (!relativePath.startsWith(this.subPath)) {
@@ -161,7 +158,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
filter: path => {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
- const relativePath = path.replace(directoryNameRegex, '');
+ const relativePath = stripFirstDirectoryFromPath(path);
if (this.subPath && !relativePath.startsWith(this.subPath)) {
return false;
}
diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts
index 2547c7b0e4..4aebff5c84 100644
--- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts
+++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts
@@ -24,11 +24,7 @@ import {
ReadTreeResponseDirOptions,
ReadTreeResponseFile,
} from '../types';
-
-// Matches a directory name + one `/` at the start of any string,
-// containing any character except / one or more times, and ending with a `/`
-// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
-const directoryNameRegex = /^[^\/]+\//;
+import { stripFirstDirectoryFromPath } from './util';
/**
* Wraps a zip archive stream into a tree response reader.
@@ -65,18 +61,13 @@ export class ZipArchiveResponse implements ReadTreeResponse {
this.read = true;
}
- // Will remove the top level dir name from the path since its name is hard to predetermine.
- private stripTopDirectory(path: string): string {
- return path.replace(directoryNameRegex, '');
- }
-
// File path relative to the root extracted directory or a sub directory if subpath is set.
private getInnerPath(path: string): string {
return path.slice(this.subPath.length);
}
private shouldBeIncluded(entry: Entry): boolean {
- const strippedPath = this.stripTopDirectory(entry.path);
+ const strippedPath = stripFirstDirectoryFromPath(entry.path);
if (this.subPath) {
if (!strippedPath.startsWith(this.subPath)) {
@@ -104,7 +95,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
if (this.shouldBeIncluded(entry)) {
files.push({
- path: this.getInnerPath(this.stripTopDirectory(entry.path)),
+ path: this.getInnerPath(stripFirstDirectoryFromPath(entry.path)),
content: () => entry.buffer(),
});
} else {
@@ -153,7 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
// as a zip can have files with directories without directory entries
if (entry.type === 'File' && this.shouldBeIncluded(entry)) {
const entryPath = this.getInnerPath(
- this.stripTopDirectory(entry.path),
+ stripFirstDirectoryFromPath(entry.path),
);
const dirname = platformPath.dirname(entryPath);
if (dirname) {
diff --git a/packages/backend-common/src/reading/tree/util.ts b/packages/backend-common/src/reading/tree/util.ts
new file mode 100644
index 0000000000..8e908a5e60
--- /dev/null
+++ b/packages/backend-common/src/reading/tree/util.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 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.
+ */
+
+// Matches a directory name + one `/` at the start of any string,
+// containing any character except `/` one or more times, and ending with a `/`
+// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
+const directoryNameRegex = /^[^\/]+\//;
+
+// Removes the first segment of a forward-slash-separated path
+export function stripFirstDirectoryFromPath(path: string): string {
+ return path.replace(directoryNameRegex, '');
+}
diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts
index c20fdcef9c..dddf6ed5d0 100644
--- a/packages/integration/src/ScmIntegrations.ts
+++ b/packages/integration/src/ScmIntegrations.ts
@@ -19,6 +19,7 @@ import { AzureIntegration } from './azure/AzureIntegration';
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
import { GitHubIntegration } from './github/GitHubIntegration';
import { GitLabIntegration } from './gitlab/GitLabIntegration';
+import { defaultScmResolveUrl } from './helpers';
import {
ScmIntegration,
ScmIntegrationRegistry,
@@ -83,11 +84,11 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
}
resolveUrl(options: { url: string; base: string }): string {
- const resolve = this.byUrl(options.base)?.resolveUrl;
- if (!resolve) {
- return new URL(options.url, options.base).toString();
+ const integration = this.byUrl(options.base);
+ if (!integration) {
+ return defaultScmResolveUrl(options);
}
- return resolve(options);
+ return integration.resolveUrl(options);
}
}
diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts
index 8a10e40bcf..3f9e165b90 100644
--- a/packages/integration/src/azure/AzureIntegration.test.ts
+++ b/packages/integration/src/azure/AzureIntegration.test.ts
@@ -58,6 +58,16 @@ describe('AzureIntegration', () => {
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
);
+ expect(
+ integration.resolveUrl({
+ url: '/a.yaml',
+ base:
+ 'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
+ }),
+ ).toBe(
+ 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
+ );
+
expect(
integration.resolveUrl({
url: './a.yaml',
diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts
index 6d57437779..2d95ef19b9 100644
--- a/packages/integration/src/azure/index.ts
+++ b/packages/integration/src/azure/index.ts
@@ -14,14 +14,15 @@
* limitations under the License.
*/
+export { AzureIntegration } from './AzureIntegration';
export {
readAzureIntegrationConfig,
readAzureIntegrationConfigs,
} from './config';
export type { AzureIntegrationConfig } from './config';
export {
+ getAzureCommitsUrl,
getAzureDownloadUrl,
getAzureFileFetchUrl,
getAzureRequestOptions,
- getAzureCommitsUrl,
} from './core';
diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts
index f3e69b946a..10b69877e4 100644
--- a/packages/integration/src/bitbucket/BitbucketIntegration.ts
+++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { basicIntegrations } from '../helpers';
+import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
import {
BitbucketIntegrationConfig,
@@ -47,4 +47,8 @@ export class BitbucketIntegration implements ScmIntegration {
get config(): BitbucketIntegrationConfig {
return this.integrationConfig;
}
+
+ resolveUrl(options: { url: string; base: string }): string {
+ return defaultScmResolveUrl(options);
+ }
}
diff --git a/packages/integration/src/bitbucket/index.ts b/packages/integration/src/bitbucket/index.ts
index 9df4d3d3fe..124fe4a2a2 100644
--- a/packages/integration/src/bitbucket/index.ts
+++ b/packages/integration/src/bitbucket/index.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+export { BitbucketIntegration } from './BitbucketIntegration';
export {
readBitbucketIntegrationConfig,
readBitbucketIntegrationConfigs,
diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts
index c103597d74..c60ab462c9 100644
--- a/packages/integration/src/github/GitHubIntegration.ts
+++ b/packages/integration/src/github/GitHubIntegration.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { basicIntegrations } from '../helpers';
+import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
import {
GitHubIntegrationConfig,
@@ -45,4 +45,8 @@ export class GitHubIntegration implements ScmIntegration {
get config(): GitHubIntegrationConfig {
return this.integrationConfig;
}
+
+ resolveUrl(options: { url: string; base: string }): string {
+ return defaultScmResolveUrl(options);
+ }
}
diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts
index 6491e8dcc5..99d2f56d0f 100644
--- a/packages/integration/src/github/index.ts
+++ b/packages/integration/src/github/index.ts
@@ -21,3 +21,4 @@ export {
export type { GitHubIntegrationConfig } from './config';
export { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core';
export { GithubCredentialsProvider } from './GithubCredentialsProvider';
+export { GitHubIntegration } from './GitHubIntegration';
diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts
index d939917366..131825edb5 100644
--- a/packages/integration/src/gitlab/GitLabIntegration.ts
+++ b/packages/integration/src/gitlab/GitLabIntegration.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { basicIntegrations } from '../helpers';
+import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
import {
GitLabIntegrationConfig,
@@ -45,4 +45,8 @@ export class GitLabIntegration implements ScmIntegration {
get config(): GitLabIntegrationConfig {
return this.integrationConfig;
}
+
+ resolveUrl(options: { url: string; base: string }): string {
+ return defaultScmResolveUrl(options);
+ }
}
diff --git a/packages/integration/src/gitlab/index.ts b/packages/integration/src/gitlab/index.ts
index 8dc4e90764..8886e0e3ce 100644
--- a/packages/integration/src/gitlab/index.ts
+++ b/packages/integration/src/gitlab/index.ts
@@ -20,3 +20,4 @@ export {
} from './config';
export type { GitLabIntegrationConfig } from './config';
export { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core';
+export { GitLabIntegration } from './GitLabIntegration';
diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts
index 9f4c531ba6..2899f2ed98 100644
--- a/packages/integration/src/helpers.test.ts
+++ b/packages/integration/src/helpers.test.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { isValidHost } from './helpers';
+import { defaultScmResolveUrl, isValidHost } from './helpers';
describe('isValidHost', () => {
it.each([
@@ -51,3 +51,69 @@ describe('isValidHost', () => {
expect(isValidHost(str)).toBe(expected);
});
});
+
+describe('defaultScmResolveUrl', () => {
+ it('works for relative paths and retains query params', () => {
+ expect(
+ defaultScmResolveUrl({
+ url: './b.yaml',
+ base:
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
+ }),
+ ).toBe(
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml',
+ );
+
+ expect(
+ defaultScmResolveUrl({
+ url: './b.yaml',
+ base:
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
+ }),
+ ).toBe(
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml?at=master',
+ );
+
+ expect(
+ defaultScmResolveUrl({
+ url: 'b.yaml',
+ base:
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
+ }),
+ ).toBe(
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml',
+ );
+ });
+
+ it('works for absolute paths and retains query params', () => {
+ expect(
+ defaultScmResolveUrl({
+ url: '/other/b.yaml',
+ base:
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
+ }),
+ ).toBe(
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml',
+ );
+
+ expect(
+ defaultScmResolveUrl({
+ url: '/other/b.yaml',
+ base:
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
+ }),
+ ).toBe(
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml?at=master',
+ );
+ });
+
+ it('works for full urls and throws away query params', () => {
+ expect(
+ defaultScmResolveUrl({
+ url: 'https://b.com/b.yaml',
+ base:
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
+ }),
+ ).toBe('https://b.com/b.yaml');
+ });
+});
diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts
index 73cef18297..c09f808011 100644
--- a/packages/integration/src/helpers.ts
+++ b/packages/integration/src/helpers.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import parseGitUrl from 'git-url-parse';
import { ScmIntegration, ScmIntegrationsGroup } from './types';
/** Checks whether the given argument is a valid URL hostname */
@@ -51,3 +52,43 @@ export function basicIntegrations(
},
};
}
+
+/**
+ * Default implementation of ScmIntegration.resolveUrl, that only works with
+ * URL pathname based providers.
+ */
+export function defaultScmResolveUrl(options: {
+ url: string;
+ base: string;
+}): string {
+ const { url, base } = options;
+
+ // If it is a fully qualified URL - then return it verbatim
+ try {
+ // eslint-disable-next-line no-new
+ new URL(url);
+ return url;
+ } catch {
+ // ignore intentionally
+ }
+
+ let updated: URL;
+
+ if (url.startsWith('/')) {
+ // If it is an absolute path, move relative to the repo root
+ const { filepath } = parseGitUrl(base);
+ updated = new URL(base);
+ const repoRootPath = updated.pathname
+ .substring(0, updated.pathname.length - filepath.length)
+ .replace(/\/+$/, '');
+ updated.pathname = `${repoRootPath}${url}`;
+ } else {
+ // For relative URLs, just let the default URL constructor handle the
+ // resolving. Note that this essentially will treat the last segment of the
+ // base as a file - NOT a folder - unless the url ends in a slash.
+ updated = new URL(url, base);
+ }
+
+ updated.search = new URL(base).search;
+ return updated.toString();
+}
diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts
index fdcd7da676..c39608fe8e 100644
--- a/packages/integration/src/index.ts
+++ b/packages/integration/src/index.ts
@@ -18,5 +18,6 @@ export * from './azure';
export * from './bitbucket';
export * from './github';
export * from './gitlab';
+export { defaultScmResolveUrl } from './helpers';
export { ScmIntegrations } from './ScmIntegrations';
export type { ScmIntegration, ScmIntegrationRegistry } from './types';
diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts
index 7d5b0fbe74..c63a323dc3 100644
--- a/packages/integration/src/types.ts
+++ b/packages/integration/src/types.ts
@@ -36,16 +36,21 @@ export interface ScmIntegration {
title: string;
/**
- * Works like the two-argument form of the URL constructor, resolving an
- * absolute or relative URL in relation to a base URL.
+ * Resolves an absolute or relative URL in relation to a base URL.
*
- * If this method is not implemented, the URL constructor is used instead for
- * URLs that match this integration.
+ * This method is adapted for use within SCM systems, so relative URLs are
+ * within the context of the root of the hierarchy pointed to by the base
+ * URL.
+ *
+ * For example, if the base URL is `/folder/a.yaml`, i.e.
+ * within the file tree of a certain repo, an absolute path of `/b.yaml` does
+ * not resolve to `https://hostname/b.yaml` but rather to
+ * `/b.yaml` inside the file tree of that same repo.
*
* @param options.url The (absolute or relative) URL or path to resolve
* @param options.base The base URL onto which this resolution happens
*/
- resolveUrl?(options: { url: string; base: string }): string;
+ resolveUrl(options: { url: string; base: string }): string;
}
/**
@@ -83,8 +88,16 @@ export interface ScmIntegrationRegistry
gitlab: ScmIntegrationsGroup;
/**
- * Works like the two-argument form of the URL constructor, resolving an
- * absolute or relative URL in relation to a base URL.
+ * Resolves an absolute or relative URL in relation to a base URL.
+ *
+ * This method is adapted for use within SCM systems, so relative URLs are
+ * within the context of the root of the hierarchy pointed to by the base
+ * URL.
+ *
+ * For example, if the base URL is `/folder/a.yaml`, i.e.
+ * within the file tree of a certain repo, an absolute path of `/b.yaml` does
+ * not resolve to `https://hostname/b.yaml` but rather to
+ * `/b.yaml` inside the file tree of that same repo.
*
* @param options.url The (absolute or relative) URL or path to resolve
* @param options.base The base URL onto which this resolution happens
From f4f4ad1f4fe6d411da72e5d91d3bd8debc2a1da5 Mon Sep 17 00:00:00 2001
From: tudi2d
Date: Thu, 11 Feb 2021 12:14:36 +0100
Subject: [PATCH 07/37] Rethink component structure to be more flexible -
Following MUI's Breadcrumbs component design
---
.../Breadcrumbs/Breadcrumbs.stories.tsx | 30 ++----
.../src/layout/Breadcrumbs/Breadcrumbs.tsx | 99 +++++++++----------
2 files changed, 57 insertions(+), 72 deletions(-)
diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
index 18569a8dad..f21b5a94e6 100644
--- a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
+++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
@@ -15,31 +15,21 @@
*/
import React from 'react';
import { Breadcrumbs } from '.';
+import { MemoryRouter } from 'react-router-dom';
+import { Link } from '../../components/Link';
export default {
title: 'Layout/Breadcrumbs',
component: Breadcrumbs,
};
-const pages = [
- {
- href: '/',
- name: 'A',
- },
- {
- href: '/',
- name: 'B',
- },
- {
- href: '/',
- name: 'C',
- },
- {
- href: '/',
- name: 'D',
- },
-];
-
// export const InHeader = () => ;
-export const OutsideOfHeader = () => ;
+export const OutsideOfHeader = () => (
+
+
+ Home
+ Home
+
+
+);
// export const ExampleUsage = () => ;
diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
index 78761d9db2..c88b446d8c 100644
--- a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
+++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
@@ -14,80 +14,75 @@
* limitations under the License.
*/
-import React from 'react';
+import React, { Fragment } from 'react';
import {
- Link,
Typography,
Breadcrumbs as MUIBreadcrumbs,
Popover,
withStyles,
} from '@material-ui/core';
-type BreadcrumbPage = {
- href: string;
- name: string;
-};
-
type BreadcrumbsProps = {
- pages: (BreadcrumbPage | BreadcrumbPage)[];
+ children?: React.ReactNode;
};
const UnderlinedText = withStyles({ root: { textDecoration: 'underline' } })(
Typography,
);
-const Breadcrumb = ({ page }: { page: BreadcrumbPage }) => (
-
- {page.name}
-
-);
+const StyledBreadcrumbs = withStyles({
+ root: {},
+ li: { textDecoration: 'underline' },
+})(MUIBreadcrumbs);
-// Should propbably take Routes instead, to work with the react-router
-export const Breadcrumbs = ({ pages }: BreadcrumbsProps) => {
+export const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
const [anchorEl, setAnchorEl] = React.useState(
null,
);
- const hasHiddenBreadcrumbs = pages.length > 3;
- const [firstPage, secondPage, ...expandablePages] = pages;
- const currentPage = pages[pages.length - 1];
- const handleClick = (event: React.MouseEvent) => {
- setAnchorEl(event.currentTarget);
- };
+ if (children instanceof Array) {
+ const [firstPage, secondPage, ...expandablePages] = children;
+ const currentPage = children[children.length - 1];
+ const hasHiddenBreadcrumbs = children.length > 3;
- const handleClose = () => {
- setAnchorEl(null);
- };
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
- const open = Boolean(anchorEl);
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+ const open = Boolean(anchorEl);
+ return (
+
+
+ {children.length > 1 && firstPage}
+ {children.length > 2 && secondPage}
+ {hasHiddenBreadcrumbs && (
+ ...
+ )}
+ {currentPage}
+
+
+ The content of the Popover.
+
+
+ );
+ }
return (
-
- {firstPage && pages.length > 1 && }
- {secondPage && pages.length > 2 && }
- {hasHiddenBreadcrumbs && (
- ...
- )}
- {currentPage && {currentPage.name}}
-
- The content of the Popover.
-
-
+ {children}
);
};
From d872f662df995c69e5d5737840f8748a09d1a7b8 Mon Sep 17 00:00:00 2001
From: Oliver Sand
Date: Thu, 11 Feb 2021 14:24:28 +0100
Subject: [PATCH 08/37] Use routed tabs to link to every settings page
---
.changeset/grumpy-cups-hope.md | 5 +++
.../src/components/SettingsPage.tsx | 40 +++++++++----------
2 files changed, 23 insertions(+), 22 deletions(-)
create mode 100644 .changeset/grumpy-cups-hope.md
diff --git a/.changeset/grumpy-cups-hope.md b/.changeset/grumpy-cups-hope.md
new file mode 100644
index 0000000000..6445c07efa
--- /dev/null
+++ b/.changeset/grumpy-cups-hope.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-user-settings': patch
+---
+
+Use routed tabs to link to every settings page.
diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx
index 0b4a86c99d..9fb576b200 100644
--- a/plugins/user-settings/src/components/SettingsPage.tsx
+++ b/plugins/user-settings/src/components/SettingsPage.tsx
@@ -14,39 +14,35 @@
* limitations under the License.
*/
-import React, { useState } from 'react';
-import { Content, Header, HeaderTabs, Page } from '@backstage/core';
-import { General } from './General';
+import { Header, Page, TabbedLayout } from '@backstage/core';
+import React from 'react';
import { AuthProviders } from './AuthProviders';
import { FeatureFlags } from './FeatureFlags';
+import { General } from './General';
type Props = {
providerSettings?: JSX.Element;
};
export const SettingsPage = ({ providerSettings }: Props) => {
- const [activeTab, setActiveTab] = useState(0);
- const onTabChange = (index: number) => {
- setActiveTab(index);
- };
-
- const tabs = [
- { id: 'general', label: 'General' },
- { id: 'auth-providers', label: 'Authentication Providers' },
- { id: 'feature-flags', label: 'Feature Flags' },
- ];
-
- const content = [
- ,
- ,
- ,
- ];
-
return (
-
- {content[activeTab]}
+
+
+
+
+
+
+
+
+
+
+
+
);
};
From 5db93ec3310eaee815b4f88be3bc692578a07c04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?=
Date: Thu, 11 Feb 2021 14:43:49 +0100
Subject: [PATCH 09/37] fix up yarn after release
---
yarn.lock | 26 ++++++++++++--------------
1 file changed, 12 insertions(+), 14 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 7db10a02e6..475ac65b6b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1827,7 +1827,7 @@
yup "^0.29.3"
"@backstage/core@^0.3.0":
- version "0.6.0"
+ version "0.6.1"
dependencies:
"@backstage/config" "^0.1.2"
"@backstage/core-api" "^0.2.8"
@@ -1866,13 +1866,13 @@
zen-observable "^0.8.15"
"@backstage/plugin-catalog@^0.2.0":
- version "0.3.0"
+ version "0.3.1"
dependencies:
- "@backstage/catalog-client" "^0.3.5"
+ "@backstage/catalog-client" "^0.3.6"
"@backstage/catalog-model" "^0.7.1"
- "@backstage/core" "^0.6.0"
- "@backstage/plugin-catalog-react" "^0.0.2"
- "@backstage/plugin-scaffolder" "^0.4.2"
+ "@backstage/core" "^0.6.1"
+ "@backstage/plugin-catalog-react" "^0.0.3"
+ "@backstage/plugin-scaffolder" "^0.5.0"
"@backstage/theme" "^0.2.3"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"
@@ -1880,7 +1880,6 @@
"@types/react" "^16.9"
classnames "^2.2.6"
git-url-parse "^11.4.4"
- moment "^2.26.0"
react "^16.13.1"
react-dom "^16.13.1"
react-helmet "6.1.0"
@@ -1890,13 +1889,13 @@
swr "^0.3.0"
"@backstage/plugin-catalog@^0.2.1":
- version "0.3.0"
+ version "0.3.1"
dependencies:
- "@backstage/catalog-client" "^0.3.5"
+ "@backstage/catalog-client" "^0.3.6"
"@backstage/catalog-model" "^0.7.1"
- "@backstage/core" "^0.6.0"
- "@backstage/plugin-catalog-react" "^0.0.2"
- "@backstage/plugin-scaffolder" "^0.4.2"
+ "@backstage/core" "^0.6.1"
+ "@backstage/plugin-catalog-react" "^0.0.3"
+ "@backstage/plugin-scaffolder" "^0.5.0"
"@backstage/theme" "^0.2.3"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"
@@ -1904,7 +1903,6 @@
"@types/react" "^16.9"
classnames "^2.2.6"
git-url-parse "^11.4.4"
- moment "^2.26.0"
react "^16.13.1"
react-dom "^16.13.1"
react-helmet "6.1.0"
@@ -17392,7 +17390,7 @@ lru-queue@0.1:
dependencies:
es5-ext "~0.10.2"
-luxon@^1.25.0:
+luxon@1.25.0, luxon@^1.25.0:
version "1.25.0"
resolved "https://registry.npmjs.org/luxon/-/luxon-1.25.0.tgz#d86219e90bc0102c0eb299d65b2f5e95efe1fe72"
integrity sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ==
From 5e74f638ce02c38760e1298b7635f9393a1eb03b Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Feb 2021 16:57:06 +0100
Subject: [PATCH 10/37] chore: add lockfile to words
---
.github/styles/vocab.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index 2991a6babd..37bfd9af92 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -119,6 +119,7 @@ Kumar
learnings
lerna
Lerna
+lockfile
Luxon
magiclink
mailto
From 5f7101d6acdea7a8dc75016bbc41bdab05f8c28b Mon Sep 17 00:00:00 2001
From: Patrik Oldsberg
Date: Thu, 11 Feb 2021 17:53:30 +0100
Subject: [PATCH 11/37] catalog-info: add links
---
catalog-info.yaml | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/catalog-info.yaml b/catalog-info.yaml
index 7e60af5755..0cab0c558a 100644
--- a/catalog-info.yaml
+++ b/catalog-info.yaml
@@ -4,6 +4,15 @@ metadata:
name: backstage
description: |
Backstage is an open-source developer portal that puts the developer experience first.
+ links:
+ - title: Website
+ url: http://backstage.io
+ - title: Documentation
+ url: https://backstage.io/docs
+ - title: Storybook
+ url: https://backstage.io/storybook
+ - title: Discord Chat
+ url: https://discord.com/invite/EBHEGzX
annotations:
github.com/project-slug: backstage/backstage
backstage.io/techdocs-ref: url:https://github.com/backstage/backstage
From 07e226872311fe8b5eab9e3a57522ba6681ab284 Mon Sep 17 00:00:00 2001
From: Iain Billett
Date: Thu, 11 Feb 2021 17:29:53 +0000
Subject: [PATCH 12/37] Export Select component from core
I saw the Select component on storybook and went
to use it but it seems it's not exported. Any chance
it could be exported?
---
.changeset/stupid-maps-do.md | 5 +++++
packages/core/src/components/index.ts | 1 +
2 files changed, 6 insertions(+)
create mode 100644 .changeset/stupid-maps-do.md
diff --git a/.changeset/stupid-maps-do.md b/.changeset/stupid-maps-do.md
new file mode 100644
index 0000000000..1cbac64f68
--- /dev/null
+++ b/.changeset/stupid-maps-do.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core': patch
+---
+
+Export Select component
diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts
index 77eba9759a..28056b33bf 100644
--- a/packages/core/src/components/index.ts
+++ b/packages/core/src/components/index.ts
@@ -31,6 +31,7 @@ export * from './MarkdownContent';
export * from './OAuthRequestDialog';
export * from './Progress';
export * from './ProgressBars';
+export * from './Select';
export * from './SimpleStepper';
export * from './Status';
export * from './StructuredMetadataTable';
From c1164fb6a8f45d7870e51c698113ba386d169b58 Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Feb 2021 20:09:24 +0100
Subject: [PATCH 13/37] bug: filepath can be returned as undefined from
`git-url-parse` let's default to empty
---
.../src/scaffolder/stages/prepare/bitbucket.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts
index d473656863..02865f8529 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts
@@ -43,7 +43,7 @@ export class BitbucketPreparer implements PreparerBase {
const targetPath = path.join(workspacePath, 'template');
const fullPathToTemplate = path.resolve(
checkoutPath,
- parsedGitUrl.filepath,
+ parsedGitUrl.filepath ?? '',
);
const git = Git.fromAuth({ logger, ...this.getAuth() });
From c6a67b100e78a0e1c34ca9b1cbd733dd4859a39c Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Feb 2021 20:10:20 +0100
Subject: [PATCH 14/37] chore: might as well do this for all parsing
---
.../scaffolder-backend/src/scaffolder/stages/prepare/github.ts | 2 +-
.../scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts
index 533aa8e005..77e6ec6d70 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts
@@ -33,7 +33,7 @@ export class GithubPreparer implements PreparerBase {
const targetPath = path.join(workspacePath, 'template');
const fullPathToTemplate = path.resolve(
checkoutPath,
- parsedGitUrl.filepath,
+ parsedGitUrl.filepath ?? '',
);
const git = this.config.token
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts
index fa5c8c1325..e15de33ac6 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts
@@ -33,7 +33,7 @@ export class GitlabPreparer implements PreparerBase {
const targetPath = path.join(workspacePath, 'template');
const fullPathToTemplate = path.resolve(
checkoutPath,
- parsedGitUrl.filepath,
+ parsedGitUrl.filepath ?? '',
);
parsedGitUrl.git_suffix = true;
From a341a8716a979364b5e801a826136ecf9c8d4bca Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Feb 2021 20:14:47 +0100
Subject: [PATCH 15/37] chore: changeset
---
.changeset/tough-worms-clap.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/tough-worms-clap.md
diff --git a/.changeset/tough-worms-clap.md b/.changeset/tough-worms-clap.md
new file mode 100644
index 0000000000..3a18593549
--- /dev/null
+++ b/.changeset/tough-worms-clap.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-backend': patch
+---
+
+Fix parsing of the path to default to empty string not undefined if git-url-parse throws something we don't expect. Fixes the error `The "path" argument must be of type string.` when preparing.
From 16fb1d03a3b898577b634d1184f374ee7937447b Mon Sep 17 00:00:00 2001
From: Gowind
Date: Thu, 11 Feb 2021 22:27:26 +0100
Subject: [PATCH 16/37] Add changeset for fixing requestLoggingHandler
---
.changeset/afraid-dingos-own.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/afraid-dingos-own.md
diff --git a/.changeset/afraid-dingos-own.md b/.changeset/afraid-dingos-own.md
new file mode 100644
index 0000000000..9f7010d330
--- /dev/null
+++ b/.changeset/afraid-dingos-own.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-common': patch
+---
+
+pass registered logger to requestLoggingHandler
From c040c9d118c192d8cffe7fade30fecadd684d2ae Mon Sep 17 00:00:00 2001
From: tudi2d
Date: Fri, 12 Feb 2021 00:27:49 +0100
Subject: [PATCH 17/37] Add hidden & layered breadcrumbs - Override style of
given components
---
.../Breadcrumbs/Breadcrumbs.stories.tsx | 114 +++++++++++++++--
.../src/layout/Breadcrumbs/Breadcrumbs.tsx | 121 ++++++++++--------
2 files changed, 170 insertions(+), 65 deletions(-)
diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
index f21b5a94e6..f2a6611d03 100644
--- a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
+++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx
@@ -13,9 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React from 'react';
-import { Breadcrumbs } from '.';
+import { Popover, Typography, Box, List, ListItem } from '@material-ui/core';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import ExpandLessIcon from '@material-ui/icons/ExpandLess';
+import React, { Fragment } from 'react';
import { MemoryRouter } from 'react-router-dom';
+import { Breadcrumbs } from '.';
import { Link } from '../../components/Link';
export default {
@@ -24,12 +27,101 @@ export default {
};
// export const InHeader = () => ;
-export const OutsideOfHeader = () => (
-
-
- Home
- Home
-
-
-);
-// export const ExampleUsage = () => ;
+export const OutsideOfHeader = () => {
+ const [anchorEl, setAnchorEl] = React.useState(
+ null,
+ );
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const open = Boolean(anchorEl);
+ return (
+
+
+ It might be the case that you want to keep your breadcrumbs outside of
+ the header. In that case, they should be positioned above the title of
+ the page.
+
+
Standard breadcrumbs
+
+ Underlined pages are links. This should show a hierarchical
+ relationship.
+
+
+ General Page
+ Second Page
+ Current page
+
+
+ General Page
+ Current page
+
+
+ Current page
+
+
+
Hidden breadcrumbs
+
+ Use this when you have more than three breadcrumbs. When user clicks on
+ ellipses, expand the breadcrumbs out.
+
+
+
+ General Page
+ Second Page
+ Third Page
+ Fourth Page
+ Current page
+
+
+
Layered breadcrumbs
+
+ Use this when you want to show alternative breadcrumbs on the same
+ hierarchical level.
+
+ Underlined pages are links. This should show a hierarchical relationship.
+
+
+
+
+
+
+);
+
export const OutsideOfHeader = () => {
const [anchorEl, setAnchorEl] = React.useState(
null,
@@ -47,23 +60,20 @@ export const OutsideOfHeader = () => {
the header. In that case, they should be positioned above the title of
the page.
+
Standard breadcrumbs
Underlined pages are links. This should show a hierarchical
relationship.
-
+
+
+
+
General Page
Second Page
Current page
-
- General Page
- Current page
-
-
- Current page
-
-
+
General Page
Second Page
Third Page
@@ -86,7 +96,7 @@ export const OutsideOfHeader = () => {
-
+
General Page
@@ -102,11 +112,11 @@ export const OutsideOfHeader = () => {
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
- horizontal: 'center',
+ horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
- horizontal: 'center',
+ horizontal: 'left',
}}
>
diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
index 6da9d7ad4d..d8cf88497f 100644
--- a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
+++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx
@@ -34,12 +34,12 @@ const ClickableText = withStyles({
},
})(Typography);
-const StyledBox = withStyles(theme => ({
+const StyledBox = withStyles({
root: {
textDecoration: 'underline',
- color: theme.palette.text.primary,
+ color: 'inherit',
},
-}))(Box);
+})(Box);
export const Breadcrumbs = ({ children, ...props }: Props) => {
const [anchorEl, setAnchorEl] = React.useState(
@@ -71,9 +71,7 @@ export const Breadcrumbs = ({ children, ...props }: Props) => {
{hasHiddenBreadcrumbs && (
...
)}
-
- {currentPage}
-
+ {currentPage} {
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
- horizontal: 'center',
+ horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx
index 98db36ab14..73fa3066ea 100644
--- a/packages/core/src/layout/Header/Header.tsx
+++ b/packages/core/src/layout/Header/Header.tsx
@@ -16,15 +16,10 @@
import React, { ReactNode, CSSProperties, PropsWithChildren } from 'react';
import { Helmet } from 'react-helmet';
-import {
- Link,
- Typography,
- Tooltip,
- makeStyles,
- Breadcrumbs,
-} from '@material-ui/core';
-import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import { Typography, Tooltip, makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
+import { Breadcrumbs } from '..';
+import { Link } from '../../components';
const useStyles = makeStyles(theme => ({
header: {
@@ -136,15 +131,9 @@ const TypeFragment = ({
}
return (
- }
- className={classes.breadcrumb}
- >
-
- {type}
-
- {pageTitle}
+
+ {type}
+ {pageTitle}
);
};
From 75cd8ac6c56d4b2b7958a3d0b05d49095f4307de Mon Sep 17 00:00:00 2001
From: tudi2d
Date: Fri, 12 Feb 2021 11:49:16 +0100
Subject: [PATCH 24/37] Add basic breadcrumbs test
---
.../layout/Breadcrumbs/Breadcrumbs.test.tsx | 39 +++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx
index f3b69cc361..0a490afb76 100644
--- a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx
+++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx
@@ -13,3 +13,42 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import { renderInTestApp } from '@backstage/test-utils';
+import { Typography } from '@material-ui/core';
+import { fireEvent } from '@testing-library/react';
+import React from 'react';
+import { Link } from '../..';
+import { Breadcrumbs } from './Breadcrumbs';
+
+describe('', () => {
+ it('should render', async () => {
+ const rendered = await renderInTestApp(
+
+ General Page
+ Current Page
+ ,
+ );
+ expect(rendered.getByLabelText('breadcrumb')).toBeVisible();
+ expect(rendered.getByText('General Page')).toBeVisible();
+ expect(rendered.getByText('Current Page')).toBeVisible();
+ });
+
+ it('should render hidden breadcrumbs', async () => {
+ const rendered = await renderInTestApp(
+
+ General Page
+ Second Page
+ Third Page
+ Fourth Page
+ Current page
+ ,
+ );
+ expect(rendered.getByText('...')).toBeVisible();
+ expect(rendered.queryByText('Third Page')).not.toBeInTheDocument();
+ expect(rendered.queryByText('Fourth Page')).not.toBeInTheDocument();
+ fireEvent.click(rendered.getByText('...'));
+ expect(rendered.getByText('Third Page')).toBeVisible();
+ expect(rendered.getByText('Fourth Page')).toBeVisible();
+ });
+});
From 688b731104b1b35e5f8a55bb60f056269c963298 Mon Sep 17 00:00:00 2001
From: tudi2d
Date: Fri, 12 Feb 2021 12:04:13 +0100
Subject: [PATCH 25/37] Add changeset
---
.changeset/wise-books-turn.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/wise-books-turn.md
diff --git a/.changeset/wise-books-turn.md b/.changeset/wise-books-turn.md
new file mode 100644
index 0000000000..d1db479ed4
--- /dev/null
+++ b/.changeset/wise-books-turn.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core': minor
+---
+
+Add Breadcrumbs component
From 914c89b1315bbbf9ad688afcaf22fbb75ad8296c Mon Sep 17 00:00:00 2001
From: Oliver Sand
Date: Fri, 12 Feb 2021 11:59:12 +0100
Subject: [PATCH 26/37] Remove the "Move repository" menu entry from the
catalog page, as it's just a placeholder
It will be easy to bring it back later, but for now it just confuses users that it's not doing anything. It's also hard to remove for integrators.
---
.changeset/five-guests-promise.md | 5 +++++
.../src/components/EntityContextMenu/EntityContextMenu.tsx | 7 -------
2 files changed, 5 insertions(+), 7 deletions(-)
create mode 100644 .changeset/five-guests-promise.md
diff --git a/.changeset/five-guests-promise.md b/.changeset/five-guests-promise.md
new file mode 100644
index 0000000000..1d4d0ae303
--- /dev/null
+++ b/.changeset/five-guests-promise.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog': patch
+---
+
+Remove the "Move repository" menu entry from the catalog page, as it's just a placeholder.
diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx
index e30f54a275..82cf556281 100644
--- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx
+++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx
@@ -25,7 +25,6 @@ import {
import { makeStyles } from '@material-ui/core/styles';
import Cancel from '@material-ui/icons/Cancel';
import MoreVert from '@material-ui/icons/MoreVert';
-import SwapHoriz from '@material-ui/icons/SwapHoriz';
import React, { useState } from 'react';
// TODO(freben): It should probably instead be the case that Header sets the theme text color to white inside itself unconditionally instead
@@ -82,12 +81,6 @@ export const EntityContextMenu = ({ onUnregisterEntity }: Props) => {
Unregister entity
-
>
From 6c4a76c595863bb501b6e13b352a46f9167c5d97 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?=
Date: Fri, 12 Feb 2021 14:24:52 +0100
Subject: [PATCH 27/37] make the template cards conform to mui standard
---
.changeset/chilly-cars-shout.md | 5 ++++
.../components/TemplateCard/TemplateCard.tsx | 23 ++++++++-----------
.../TemplatePage/TemplatePage.test.tsx | 6 ++---
.../components/TemplatePage/TemplatePage.tsx | 4 ++--
4 files changed, 19 insertions(+), 19 deletions(-)
create mode 100644 .changeset/chilly-cars-shout.md
diff --git a/.changeset/chilly-cars-shout.md b/.changeset/chilly-cars-shout.md
new file mode 100644
index 0000000000..60871b07a3
--- /dev/null
+++ b/.changeset/chilly-cars-shout.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder': patch
+---
+
+Make the `TemplateCard` conform to what material-ui recommends in their examples. This fixes the extra padding around the buttons.
diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx
index 732b5f3e44..ef17335b26 100644
--- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx
+++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx
@@ -17,6 +17,8 @@ import { Button } from '@backstage/core';
import { BackstageTheme, pageTheme } from '@backstage/theme';
import {
Card,
+ CardActions,
+ CardContent,
Chip,
makeStyles,
Typography,
@@ -34,18 +36,11 @@ const useStyles = makeStyles(theme => ({
props.backgroundImage,
backgroundPosition: 0,
},
- content: {
- padding: theme.spacing(2),
- },
description: {
height: 175,
overflow: 'hidden',
textOverflow: 'ellipsis',
},
- footer: {
- display: 'flex',
- flexDirection: 'row-reverse',
- },
}));
export type TemplateCardProps = {
@@ -76,19 +71,19 @@ export const TemplateCard = ({
{type}{title}
-
+
{tags?.map(tag => (
))}
{description}
-
-
-
-
+
+
+
+
);
};
diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx
index f3f97be877..09df6dba1a 100644
--- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx
+++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx
@@ -99,7 +99,7 @@ describe('TemplatePage', () => {
,
);
- expect(rendered.queryByText('Create a new component')).toBeInTheDocument();
+ expect(rendered.queryByText('Create a New Component')).toBeInTheDocument();
expect(rendered.queryByText('React SSR Template')).toBeInTheDocument();
// await act(async () => await mutate('templates/test'));
});
@@ -116,7 +116,7 @@ describe('TemplatePage', () => {
,
);
- expect(rendered.queryByText('Create a new component')).toBeInTheDocument();
+ expect(rendered.queryByText('Create a New Component')).toBeInTheDocument();
expect(rendered.queryByTestId('loading-progress')).toBeInTheDocument();
// Need to cleanup the promise or will timeout
act(() => {
@@ -141,7 +141,7 @@ describe('TemplatePage', () => {
);
expect(
- rendered.queryByText('Create a new component'),
+ rendered.queryByText('Create a New Component'),
).not.toBeInTheDocument();
expect(rendered.queryByText('This is root')).toBeInTheDocument();
});
diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx
index e1422d4ec1..d779f8b42c 100644
--- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx
+++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx
@@ -149,10 +149,10 @@ export const TemplatePage = () => {
return (
- Create a new component
+ Create a New Component
>
}
subtitle="Create new software components using standard templates"
From 48b0db4aa25d921c25a173c2a969df4d448c77a2 Mon Sep 17 00:00:00 2001
From: tudi2d
Date: Fri, 12 Feb 2021 15:47:40 +0100
Subject: [PATCH 28/37] Treat as patch instead of minor change
---
.changeset/wise-books-turn.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.changeset/wise-books-turn.md b/.changeset/wise-books-turn.md
index d1db479ed4..77a42c5304 100644
--- a/.changeset/wise-books-turn.md
+++ b/.changeset/wise-books-turn.md
@@ -1,5 +1,5 @@
---
-'@backstage/core': minor
+'@backstage/core': patch
---
Add Breadcrumbs component
From d84df12d3b03424663ffe54f6844db9a6ec58dde Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Feb 2021 22:48:57 +0100
Subject: [PATCH 29/37] bug: fixing the input path to the FilePreparer, and
fixing the destination directory for the preparer to template
---
.../src/scaffolder/stages/prepare/file.ts | 6 +++---
plugins/scaffolder-backend/src/service/router.ts | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
index 4d49ba222e..7e4e542165 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
@@ -25,12 +25,12 @@ export class FilePreparer implements PreparerBase {
throw new InputError(`Wrong location protocol, should be 'file', ${url}`);
}
- const checkoutDir = path.join(workspacePath, 'checkout');
- await fs.ensureDir(checkoutDir);
+ const templateDir = path.join(workspacePath, 'template');
+ await fs.ensureDir(templateDir);
const templatePath = fileURLToPath(url);
- await fs.copy(templatePath, checkoutDir, {
+ await fs.copy(templatePath, templateDir, {
recursive: true,
});
}
diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts
index 51425f75e8..e8aa9e7c43 100644
--- a/plugins/scaffolder-backend/src/service/router.ts
+++ b/plugins/scaffolder-backend/src/service/router.ts
@@ -17,7 +17,7 @@
import { Config } from '@backstage/config';
import Docker from 'dockerode';
import express from 'express';
-import { resolve as resolvePath } from 'path';
+import { resolve as resolvePath, dirname } from 'path';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import {
@@ -164,7 +164,7 @@ export async function createRouter(
const preparer = new FilePreparer();
const path = resolvePath(
- templateEntityLocation,
+ dirname(templateEntityLocation),
template.spec.path || '.',
);
From 3957c024f555e419b651b871a0833e818ebdaba8 Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Feb 2021 22:54:20 +0100
Subject: [PATCH 30/37] chore: renaming to use targetPath rather than
templatePath like the rest of the preparers
---
.../src/scaffolder/stages/prepare/file.test.ts | 6 +++---
.../src/scaffolder/stages/prepare/file.ts | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts
index 0c94edce42..f6e5600a83 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts
@@ -28,7 +28,7 @@ describe('File preparer', () => {
const preparer = new FilePreparer();
const root = os.platform() === 'win32' ? 'C:\\' : '/';
const workspacePath = path.join(root, 'tmp');
- const checkoutPath = path.resolve(workspacePath, 'checkout');
+ const targetPath = path.resolve(workspacePath, 'template');
await preparer.prepare({
url: `file:///${root}path/to/template`,
@@ -37,12 +37,12 @@ describe('File preparer', () => {
});
expect(fs.copy).toHaveBeenCalledWith(
path.join(root, 'path', 'to', 'template'),
- checkoutPath,
+ targetPath,
{
recursive: true,
},
);
- expect(fs.ensureDir).toHaveBeenCalledWith(checkoutPath);
+ expect(fs.ensureDir).toHaveBeenCalledWith(targetPath);
await expect(
preparer.prepare({
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
index 7e4e542165..687cc39452 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
@@ -25,12 +25,12 @@ export class FilePreparer implements PreparerBase {
throw new InputError(`Wrong location protocol, should be 'file', ${url}`);
}
- const templateDir = path.join(workspacePath, 'template');
- await fs.ensureDir(templateDir);
+ const targetDir = path.join(workspacePath, 'template');
+ await fs.ensureDir(targetDir);
const templatePath = fileURLToPath(url);
- await fs.copy(templatePath, templateDir, {
+ await fs.copy(templatePath, targetDir, {
recursive: true,
});
}
From 29c8bcc532e5f4b6b42918c0220c03be82db8585 Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Feb 2021 22:58:47 +0100
Subject: [PATCH 31/37] chore: added changeset
---
.changeset/soft-rings-obey.md | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 .changeset/soft-rings-obey.md
diff --git a/.changeset/soft-rings-obey.md b/.changeset/soft-rings-obey.md
new file mode 100644
index 0000000000..792b023785
--- /dev/null
+++ b/.changeset/soft-rings-obey.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-scaffolder-backend': patch
+---
+
+Fixed the `prepare` step for when using local templates that were added to the catalog using the `file:` target configuration.
+No more `EPERM: operation not permitted` error messages. �
From 8bed34a764d33dfd2460bfdd66dbdea6d070449f Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Feb 2021 23:54:57 +0100
Subject: [PATCH 32/37] chore: remove the random characters that were not
artifacts of me trying to quit vim.
---
.changeset/soft-rings-obey.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.changeset/soft-rings-obey.md b/.changeset/soft-rings-obey.md
index 792b023785..91308bdc50 100644
--- a/.changeset/soft-rings-obey.md
+++ b/.changeset/soft-rings-obey.md
@@ -3,4 +3,4 @@
---
Fixed the `prepare` step for when using local templates that were added to the catalog using the `file:` target configuration.
-No more `EPERM: operation not permitted` error messages. �
+No more `EPERM: operation not permitted` error messages.
From d9687c524f3279de384ee4a3c2bf577ce61dd635 Mon Sep 17 00:00:00 2001
From: Patrik Oldsberg
Date: Sun, 14 Feb 2021 20:00:15 +0100
Subject: [PATCH 33/37] auth-backend: fix key timestamp parsing with sqlite
---
.changeset/rich-apricots-lick.md | 5 +++++
plugins/auth-backend/src/identity/DatabaseKeyStore.ts | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
create mode 100644 .changeset/rich-apricots-lick.md
diff --git a/.changeset/rich-apricots-lick.md b/.changeset/rich-apricots-lick.md
new file mode 100644
index 0000000000..f9daf861d2
--- /dev/null
+++ b/.changeset/rich-apricots-lick.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-auth-backend': patch
+---
+
+Fixed parsing of OIDC key timestamps when using SQLite.
diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
index e53102e333..dd2941a3d5 100644
--- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
+++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
@@ -39,7 +39,7 @@ type Options = {
const parseDate = (date: string | Date) => {
const parsedDate =
typeof date === 'string'
- ? DateTime.fromSQL(date, { locale: 'UTC' })
+ ? DateTime.fromSQL(date, { zone: 'UTC' })
: DateTime.fromJSDate(date);
if (!parsedDate.isValid) {
From 68aba8682b46eaf1776c8e9e2e7c4cf72ff126bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?=
Date: Mon, 15 Feb 2021 10:21:05 +0100
Subject: [PATCH 34/37] fix a few little things in the entity docs
---
.../software-catalog/descriptor-format.md | 25 ++++++-------------
1 file changed, 7 insertions(+), 18 deletions(-)
diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md
index 1aab1268cd..6658dfc055 100644
--- a/docs/features/software-catalog/descriptor-format.md
+++ b/docs/features/software-catalog/descriptor-format.md
@@ -205,7 +205,8 @@ described below.
In addition to these, you may add any number of other fields directly under
`metadata`, but be aware that general plugins and tools may not be able to
-understand their semantics.
+understand their semantics. See [Extending the model](extending-the-model.md)
+for more information.
### `name` [required]
@@ -214,8 +215,8 @@ entity, and for machines and other components to reference the entity (e.g. in
URLs or from other entity specification files).
Names must be unique per kind, within a given namespace (if specified), at any
-point in time. Names may be reused at a later time, after an entity is deleted
-from the registry.
+point in time. This uniqueness constraint is case insensitive. Names may be
+reused at a later time, after an entity is deleted from the registry.
Names are required to follow a certain format. Entities that do not follow those
rules will not be accepted for registration in the catalog. The ruleset is
@@ -226,19 +227,7 @@ follows.
- Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of
`[-_.]`
-Example: `visits-tracking-service`, `CircleciBuildsDump_avro_gcs`
-
-In addition to this, names are passed through a normalization function and then
-compared to the same normalized form of other entity names and made sure to not
-collide. This rule of uniqueness exists to avoid situations where e.g. both
-`my-component` and `MyComponent` are registered side by side, which leads to
-confusion and risk. The normalization function is also configurable, but the
-default behavior is as follows.
-
-- Strip out all characters outside of the set `[a-zA-Z0-9]`
-- Convert to lowercase
-
-Example: `CircleciBuildsDs_avro_gcs` -> `circlecibuildsdsavrogcs`
+Example: `visits-tracking-service`, `CircleciBuildsDumpV2_avro_gcs`
### `namespace` [optional]
@@ -248,7 +237,8 @@ the same format restrictions as `name` above.
This field is optional, and currently has no special semantics apart from
bounding the name uniqueness constraint if specified. It is reserved for future
use and may get broader semantic implication later. For now, it is recommended
-to not specify a namespace unless you have specific need to do so.
+to not specify a namespace unless you have specific need to do so. This means
+the entity belongs to the `"default"` namespace.
Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities,
i.e. not Backstage specific but the same as in Kubernetes.
@@ -278,7 +268,6 @@ most 253 characters in total. The name part must be sequences of `[a-zA-Z0-9]`
separated by any of `[-_.]`, at most 63 characters in total.
The `backstage.io/` prefix is reserved for use by Backstage core components.
-Some keys such as `system` also have predefined semantics.
Values are strings that follow the same restrictions as `name` above.
From 32a9504095a5eae3127b5d344dfa2b82311cee2c Mon Sep 17 00:00:00 2001
From: Oliver Sand
Date: Mon, 15 Feb 2021 11:34:51 +0100
Subject: [PATCH 35/37] Hide the kind of the owner if it's the default kind for
the `ownedBy` relationship (group).
I guess I missed this one. In the catalog table & co we already hide it.
Signed-off-by: Oliver Sand oliver.sand@sda-se.com
---
.changeset/cuddly-bags-share.md | 6 ++++++
plugins/catalog/src/components/AboutCard/AboutContent.tsx | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
create mode 100644 .changeset/cuddly-bags-share.md
diff --git a/.changeset/cuddly-bags-share.md b/.changeset/cuddly-bags-share.md
new file mode 100644
index 0000000000..6aff6a9404
--- /dev/null
+++ b/.changeset/cuddly-bags-share.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-catalog': patch
+---
+
+Hide the kind of the owner if it's the default kind for the `ownedBy`
+relationship (group).
diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx
index 75b7827111..57ea894570 100644
--- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx
+++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx
@@ -66,7 +66,7 @@ export const AboutContent = ({ entity }: Props) => {
-
+
{isSystem && (
Date: Mon, 15 Feb 2021 11:12:53 +0100
Subject: [PATCH 36/37] Export createExternalRouteRef and set id
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Fredrik Adelöw
Co-authored-by: blam
Co-authored-by: Patrik Oldsberg
---
docs/plugins/composability.md | 2 +-
packages/core-api/src/app/App.test.tsx | 8 ++++----
packages/core-api/src/routing/RouteRef.ts | 21 ++++++++++++++------
packages/core-api/src/routing/hooks.test.tsx | 6 +++---
packages/core-api/src/routing/hooks.tsx | 2 +-
packages/core-api/src/routing/index.ts | 2 +-
6 files changed, 25 insertions(+), 16 deletions(-)
diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md
index a496217875..6106fa9141 100644
--- a/docs/plugins/composability.md
+++ b/docs/plugins/composability.md
@@ -248,7 +248,7 @@ might be linking to, allowing the app to decide the final target. If the
declare an `ExternalRouteRef` similar to this:
```ts
-const headerLinkRouteRef = createExternalRouteRef();
+const headerLinkRouteRef = createExternalRouteRef({ id: 'header-link' });
```
### Binding External Routes in the App
diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx
index 8dd40d52b0..6a1e80334b 100644
--- a/packages/core-api/src/app/App.test.tsx
+++ b/packages/core-api/src/app/App.test.tsx
@@ -28,7 +28,7 @@ import { generateBoundRoutes, PrivateAppImpl } from './App';
describe('generateBoundRoutes', () => {
it('runs happy path', () => {
- const external = { myRoute: createExternalRouteRef() };
+ const external = { myRoute: createExternalRouteRef({ id: '1' }) };
const ref = createRouteRef({ path: '', title: '' });
const result = generateBoundRoutes(({ bind }) => {
bind(external, { myRoute: ref });
@@ -38,7 +38,7 @@ describe('generateBoundRoutes', () => {
});
it('throws on unknown keys', () => {
- const external = { myRoute: createExternalRouteRef() };
+ const external = { myRoute: createExternalRouteRef({ id: '2' }) };
const ref = createRouteRef({ path: '', title: '' });
expect(() =>
generateBoundRoutes(({ bind }) => {
@@ -51,7 +51,7 @@ describe('generateBoundRoutes', () => {
describe('Integration Test', () => {
const plugin1RouteRef = createRouteRef({ path: '/blah1', title: '' });
const plugin2RouteRef = createRouteRef({ path: '/blah2', title: '' });
- const externalRouteRef = createExternalRouteRef();
+ const externalRouteRef = createExternalRouteRef({ id: '3' });
const plugin1 = createPlugin({
id: 'blob',
@@ -77,7 +77,7 @@ describe('Integration Test', () => {
Promise.resolve((_: PropsWithChildren<{ path?: string }>) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const routeRefFunction = useRouteRef(externalRouteRef);
- return
Our Route Is: {routeRefFunction({})}
;
+ return
Our Route Is: {routeRefFunction()}
;
}),
mountPoint: plugin1RouteRef,
}),
diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts
index 0a3df49b58..5ae85ab553 100644
--- a/packages/core-api/src/routing/RouteRef.ts
+++ b/packages/core-api/src/routing/RouteRef.ts
@@ -65,13 +65,22 @@ export function createRouteRef<
}
export class ExternalRouteRef {
- private constructor() {}
-
- toString() {
- return `externalRouteRef{}`;
+ private constructor(id: string) {
+ this.toString = () => `externalRouteRef{${id}}`;
}
}
-export function createExternalRouteRef(): ExternalRouteRef {
- return new ((ExternalRouteRef as unknown) as { new (): ExternalRouteRef })();
+export type ExternalRouteRefOptions = {
+ /**
+ * An identifier for this route, used to identify it in error messages
+ */
+ id: string;
+};
+
+export function createExternalRouteRef(
+ options: ExternalRouteRefOptions,
+): ExternalRouteRef {
+ return new ((ExternalRouteRef as unknown) as {
+ new (id: string): ExternalRouteRef;
+ })(options.id);
}
diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx
index 84a8314807..462b44c3bd 100644
--- a/packages/core-api/src/routing/hooks.test.tsx
+++ b/packages/core-api/src/routing/hooks.test.tsx
@@ -59,9 +59,9 @@ const ref2 = createRouteRef(mockConfig({ path: '/wat2' }));
const ref3 = createRouteRef(mockConfig({ path: '/wat3' }));
const ref4 = createRouteRef(mockConfig({ path: '/wat4' }));
const ref5 = createRouteRef(mockConfig({ path: '/wat5' }));
-const eRefA = createExternalRouteRef();
-const eRefB = createExternalRouteRef();
-const eRefC = createExternalRouteRef();
+const eRefA = createExternalRouteRef({ id: '1' });
+const eRefB = createExternalRouteRef({ id: '2' });
+const eRefC = createExternalRouteRef({ id: '3' });
const MockRouteSource = (props: {
path?: string;
diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx
index 3a18d8b0af..c478027ae2 100644
--- a/packages/core-api/src/routing/hooks.tsx
+++ b/packages/core-api/src/routing/hooks.tsx
@@ -111,7 +111,7 @@ class RouteResolver {
const RoutingContext = createContext(undefined);
-export function useRouteRef(
+export function useRouteRef(
routeRef: RouteRef | ExternalRouteRef,
): RouteFunc {
const sourceLocation = useLocation();
diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts
index af71e0c3e9..ad88f8ff02 100644
--- a/packages/core-api/src/routing/index.ts
+++ b/packages/core-api/src/routing/index.ts
@@ -21,6 +21,6 @@ export type {
MutableRouteRef,
} from './types';
export { FlatRoutes } from './FlatRoutes';
-export { createRouteRef } from './RouteRef';
+export { createRouteRef, createExternalRouteRef } from './RouteRef';
export type { RouteRefConfig } from './RouteRef';
export { useRouteRef } from './hooks';
From fd3f2a8c0cd4302517c0a7ebc81f505ea8dffc89 Mon Sep 17 00:00:00 2001
From: Johan Haals
Date: Mon, 15 Feb 2021 12:02:03 +0100
Subject: [PATCH 37/37] Add changeset
---
.changeset/fresh-seals-retire.md | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 .changeset/fresh-seals-retire.md
diff --git a/.changeset/fresh-seals-retire.md b/.changeset/fresh-seals-retire.md
new file mode 100644
index 0000000000..f58eebef4e
--- /dev/null
+++ b/.changeset/fresh-seals-retire.md
@@ -0,0 +1,6 @@
+---
+'@backstage/core-api': patch
+'@backstage/core': patch
+---
+
+Export `createExternalRouteRef`, as well as give it an `id` for easier debugging, and fix parameter requirements when used with `useRouteRef`.