Merge branch 'backstage:master' into topic/add-homepagetimer-to-new-homepage
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Fixing issue with `AzureUrlReader` that doesn't do `subpath` directories correctly
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Disable all buttons in the final step when 'Create' button is clicked in template.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Make ownership card style customizable via custom `theme.getPageTheme()`.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Use `ScmIntegrationRegistry#resolveUrl` in the placeholder processors instead of a custom implementation.
|
||||
|
||||
If you manually instantiate the `PlaceholderProcessor` (you most probably don't), add the new required constructor parameter:
|
||||
|
||||
```diff
|
||||
+ import { ScmIntegrations } from '@backstage/integration';
|
||||
// ...
|
||||
+ const integrations = ScmIntegrations.fromConfig(config);
|
||||
// ...
|
||||
new PlaceholderProcessor({
|
||||
resolvers: placeholderResolvers,
|
||||
reader,
|
||||
+ integrations,
|
||||
});
|
||||
```
|
||||
|
||||
All custom `PlaceholderResolver` can use the new `resolveUrl` parameter to resolve relative URLs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Use correct `Link` in ownership card to avoid a full reload of the app while navigating.
|
||||
@@ -220,6 +220,21 @@ describe('AzureUrlReader', () => {
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('returns the wanted files from an archive when a subpath is passed through', async () => {
|
||||
const response = await processor.readTree(
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fdocs',
|
||||
);
|
||||
|
||||
expect(response.etag).toBe('123abc2');
|
||||
|
||||
const files = await response.files();
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
const indexMarkdownFile = await files[0].content();
|
||||
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files', async () => {
|
||||
const response = await processor.readTree(
|
||||
'https://dev.azure.com/organization/project/_git/repository',
|
||||
|
||||
@@ -94,8 +94,6 @@ export class AzureUrlReader implements UrlReader {
|
||||
url: string,
|
||||
options?: ReadTreeOptions,
|
||||
): Promise<ReadTreeResponse> {
|
||||
// TODO: Support filepath based reading tree feature like other providers
|
||||
|
||||
// Get latest commit SHA
|
||||
|
||||
const commitsAzureResponse = await fetch(
|
||||
@@ -129,10 +127,13 @@ export class AzureUrlReader implements UrlReader {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const { filepath } = parseGitUrl(url);
|
||||
|
||||
return await this.deps.treeResponseFactory.fromZipArchive({
|
||||
stream: archiveAzureResponse.body as unknown as Readable,
|
||||
etag: commitSha,
|
||||
filter: options?.filter,
|
||||
subpath: filepath,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -62,36 +62,6 @@ describe('ZipArchiveResponse', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should read files and strip root dir if requested', async () => {
|
||||
const stream = fs.createReadStream('/test-archive-with-extra-root-dir.zip');
|
||||
|
||||
const res = new ZipArchiveResponse(
|
||||
stream,
|
||||
'',
|
||||
'/tmp',
|
||||
'etag',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
const files = await res.files();
|
||||
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'mkdocs.yml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
{
|
||||
path: 'docs/index.md',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
const contents = await Promise.all(files.map(f => f.content()));
|
||||
expect(contents.map(c => c.toString('utf8').trim())).toEqual([
|
||||
'site_name: Test',
|
||||
'# Test',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should read files with filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
ReadTreeResponseDirOptions,
|
||||
ReadTreeResponseFile,
|
||||
} from '../types';
|
||||
import { stripFirstDirectoryFromPath } from './util';
|
||||
|
||||
/**
|
||||
* Wraps a zip archive stream into a tree response reader.
|
||||
@@ -38,7 +37,6 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
private readonly workDir: string,
|
||||
public readonly etag: string,
|
||||
private readonly filter?: (path: string, info: { size: number }) => boolean,
|
||||
private readonly stripFirstDirectory?: boolean,
|
||||
) {
|
||||
if (subPath) {
|
||||
if (!subPath.endsWith('/')) {
|
||||
@@ -68,12 +66,8 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
}
|
||||
|
||||
private shouldBeIncluded(entry: Entry): boolean {
|
||||
const strippedPath = this.stripFirstDirectory
|
||||
? stripFirstDirectoryFromPath(entry.path)
|
||||
: entry.path;
|
||||
|
||||
if (this.subPath) {
|
||||
if (!strippedPath.startsWith(this.subPath)) {
|
||||
if (!entry.path.startsWith(this.subPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -102,11 +96,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
|
||||
if (this.shouldBeIncluded(entry)) {
|
||||
files.push({
|
||||
path: this.getInnerPath(
|
||||
this.stripFirstDirectory
|
||||
? stripFirstDirectoryFromPath(entry.path)
|
||||
: entry.path,
|
||||
),
|
||||
path: this.getInnerPath(entry.path),
|
||||
content: () => entry.buffer(),
|
||||
});
|
||||
} else {
|
||||
@@ -154,11 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
// Ignore directory entries since we handle that with the file entries
|
||||
// as a zip can have files with directories without directory entries
|
||||
if (entry.type === 'File' && this.shouldBeIncluded(entry)) {
|
||||
const entryPath = this.getInnerPath(
|
||||
this.stripFirstDirectory
|
||||
? stripFirstDirectoryFromPath(entry.path)
|
||||
: entry.path,
|
||||
);
|
||||
const entryPath = this.getInnerPath(entry.path);
|
||||
const dirname = platformPath.dirname(entryPath);
|
||||
if (dirname) {
|
||||
await fs.mkdirp(platformPath.join(dir, dirname));
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import {
|
||||
jsonPlaceholderResolver,
|
||||
PlaceholderProcessor,
|
||||
@@ -25,6 +27,8 @@ import {
|
||||
yamlPlaceholderResolver,
|
||||
} from './PlaceholderProcessor';
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(new ConfigReader({}));
|
||||
|
||||
describe('PlaceholderProcessor', () => {
|
||||
const read: jest.MockedFunction<ResolverRead> = jest.fn();
|
||||
const reader: UrlReader = { read, readTree: jest.fn(), search: jest.fn() };
|
||||
@@ -44,6 +48,7 @@ describe('PlaceholderProcessor', () => {
|
||||
foo: async () => 'replaced',
|
||||
},
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
await expect(
|
||||
processor.preProcessEntity(input, { type: 't', target: 'l' }),
|
||||
@@ -59,6 +64,7 @@ describe('PlaceholderProcessor', () => {
|
||||
upper: upperResolver,
|
||||
},
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -95,6 +101,7 @@ describe('PlaceholderProcessor', () => {
|
||||
bar: jest.fn(),
|
||||
},
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
const entity: Entity = {
|
||||
apiVersion: 'a',
|
||||
@@ -115,6 +122,7 @@ describe('PlaceholderProcessor', () => {
|
||||
bar: jest.fn(),
|
||||
},
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
const entity: Entity = {
|
||||
apiVersion: 'a',
|
||||
@@ -134,6 +142,7 @@ describe('PlaceholderProcessor', () => {
|
||||
const processor = new PlaceholderProcessor({
|
||||
resolvers: { text: textPlaceholderResolver },
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -169,6 +178,7 @@ describe('PlaceholderProcessor', () => {
|
||||
const processor = new PlaceholderProcessor({
|
||||
resolvers: { json: jsonPlaceholderResolver },
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -202,6 +212,7 @@ describe('PlaceholderProcessor', () => {
|
||||
const processor = new PlaceholderProcessor({
|
||||
resolvers: { yaml: yamlPlaceholderResolver },
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -235,6 +246,7 @@ describe('PlaceholderProcessor', () => {
|
||||
const processor = new PlaceholderProcessor({
|
||||
resolvers: { text: textPlaceholderResolver },
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -272,6 +284,7 @@ describe('PlaceholderProcessor', () => {
|
||||
const processor = new PlaceholderProcessor({
|
||||
resolvers: { text: textPlaceholderResolver },
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -311,6 +324,7 @@ describe('PlaceholderProcessor', () => {
|
||||
const processor = new PlaceholderProcessor({
|
||||
resolvers: { text: textPlaceholderResolver },
|
||||
reader,
|
||||
integrations,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -331,7 +345,7 @@ describe('PlaceholderProcessor', () => {
|
||||
},
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Placeholder $text could not form a URL out of ./a/b/catalog-info.yaml and ../c/catalog-info.yaml',
|
||||
'Placeholder $text could not form a URL out of ./a/b/catalog-info.yaml and ../c/catalog-info.yaml, TypeError: Invalid base URL: ./a/b/catalog-info.yaml',
|
||||
);
|
||||
|
||||
expect(read).not.toBeCalled();
|
||||
@@ -345,6 +359,7 @@ describe('yamlPlaceholderResolver', () => {
|
||||
value: './file.yaml',
|
||||
baseUrl: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml',
|
||||
read,
|
||||
resolveUrl: (url, base) => integrations.resolveUrl({ url, base }),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -389,6 +404,7 @@ describe('jsonPlaceholderResolver', () => {
|
||||
value: './file.json',
|
||||
baseUrl: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml',
|
||||
read,
|
||||
resolveUrl: (url, base) => integrations.resolveUrl({ url, base }),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -17,16 +17,19 @@
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Entity, LocationSpec } from '@backstage/catalog-model';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import yaml from 'yaml';
|
||||
import { CatalogProcessor } from './types';
|
||||
|
||||
export type ResolverRead = (url: string) => Promise<Buffer>;
|
||||
export type ResolverResolveUrl = (url: string, base: string) => string;
|
||||
|
||||
export type ResolverParams = {
|
||||
key: string;
|
||||
value: JsonValue;
|
||||
baseUrl: string;
|
||||
read: ResolverRead;
|
||||
resolveUrl: ResolverResolveUrl;
|
||||
};
|
||||
|
||||
export type PlaceholderResolver = (
|
||||
@@ -36,6 +39,7 @@ export type PlaceholderResolver = (
|
||||
type Options = {
|
||||
resolvers: Record<string, PlaceholderResolver>;
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrationRegistry;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -103,12 +107,19 @@ export class PlaceholderProcessor implements CatalogProcessor {
|
||||
return this.options.reader.read(url);
|
||||
};
|
||||
|
||||
const resolveUrl = (url: string, base: string): string =>
|
||||
this.options.integrations.resolveUrl({
|
||||
url,
|
||||
base,
|
||||
});
|
||||
|
||||
return [
|
||||
await resolver({
|
||||
key: resolverKey,
|
||||
value: resolverValue,
|
||||
baseUrl: location.target,
|
||||
read,
|
||||
resolveUrl,
|
||||
}),
|
||||
true,
|
||||
];
|
||||
@@ -191,31 +202,27 @@ async function readTextLocation(params: ResolverParams): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
function relativeUrl({ key, value, baseUrl }: ResolverParams): string {
|
||||
function relativeUrl({
|
||||
key,
|
||||
value,
|
||||
baseUrl,
|
||||
resolveUrl,
|
||||
}: ResolverParams): string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(
|
||||
`Placeholder \$${key} expected a string value parameter, in the form of an absolute URL or a relative path`,
|
||||
);
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
// The two-value form of the URL constructor handles relative paths for us
|
||||
url = new URL(value, baseUrl);
|
||||
} catch {
|
||||
try {
|
||||
// Check whether value is a valid absolute URL on it's own, if not fail.
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
// The only remaining case that isn't support is a relative file path that should be
|
||||
// resolved using a relative file location. Accessing local file paths can lead to
|
||||
// path traversal attacks and access to any file on the host system. Implementing this
|
||||
// would require additional security measures.
|
||||
throw new Error(
|
||||
`Placeholder \$${key} could not form a URL out of ${baseUrl} and ${value}`,
|
||||
);
|
||||
}
|
||||
return resolveUrl(value, baseUrl);
|
||||
} catch (e) {
|
||||
// The only remaining case that isn't support is a relative file path that should be
|
||||
// resolved using a relative file location. Accessing local file paths can lead to
|
||||
// path traversal attacks and access to any file on the host system. Implementing this
|
||||
// would require additional security measures.
|
||||
throw new Error(
|
||||
`Placeholder \$${key} could not form a URL out of ${baseUrl} and ${value}, ${e}`,
|
||||
);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
@@ -385,7 +385,11 @@ export class NextCatalogBuilder {
|
||||
|
||||
// These are always there no matter what
|
||||
const processors: CatalogProcessor[] = [
|
||||
new PlaceholderProcessor({ resolvers: placeholderResolvers, reader }),
|
||||
new PlaceholderProcessor({
|
||||
resolvers: placeholderResolvers,
|
||||
reader,
|
||||
integrations,
|
||||
}),
|
||||
new BuiltinKindsEntityProcessor(),
|
||||
];
|
||||
|
||||
|
||||
@@ -306,7 +306,11 @@ export class CatalogBuilder {
|
||||
// These are always there no matter what
|
||||
const processors: CatalogProcessor[] = [
|
||||
StaticLocationProcessor.fromConfig(config),
|
||||
new PlaceholderProcessor({ resolvers: placeholderResolvers, reader }),
|
||||
new PlaceholderProcessor({
|
||||
resolvers: placeholderResolvers,
|
||||
reader,
|
||||
integrations,
|
||||
}),
|
||||
new BuiltinKindsEntityProcessor(),
|
||||
];
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
InfoCard,
|
||||
InfoCardVariants,
|
||||
Link,
|
||||
Progress,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
@@ -29,18 +30,16 @@ import {
|
||||
isOwnerOf,
|
||||
useEntity,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { BackstageTheme, genPageTheme } from '@backstage/theme';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import {
|
||||
Box,
|
||||
createStyles,
|
||||
Grid,
|
||||
Link,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import qs from 'qs';
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
type EntityTypeProps = {
|
||||
@@ -49,16 +48,6 @@ type EntityTypeProps = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
const createPageTheme = (
|
||||
theme: BackstageTheme,
|
||||
shapeKey: string,
|
||||
colorsKey: string,
|
||||
) => {
|
||||
const { colors } = theme.getPageTheme({ themeId: colorsKey });
|
||||
const { shape } = theme.getPageTheme({ themeId: shapeKey });
|
||||
return genPageTheme(colors, shape).backgroundImage;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme: BackstageTheme) =>
|
||||
createStyles({
|
||||
card: {
|
||||
@@ -77,7 +66,7 @@ const useStyles = makeStyles((theme: BackstageTheme) =>
|
||||
},
|
||||
entityTypeBox: {
|
||||
background: (props: { type: string }) =>
|
||||
createPageTheme(theme, props.type, props.type),
|
||||
theme.getPageTheme({ themeId: props.type }).backgroundImage,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -96,7 +85,7 @@ const EntityCountTile = ({
|
||||
const classes = useStyles({ type });
|
||||
|
||||
return (
|
||||
<Link href={url} variant="body2">
|
||||
<Link to={url} variant="body2">
|
||||
<Box
|
||||
className={`${classes.card} ${classes.entityTypeBox}`}
|
||||
display="flex"
|
||||
@@ -218,7 +207,7 @@ export const OwnershipCard = ({
|
||||
counter={c.counter}
|
||||
type={c.type}
|
||||
name={c.name}
|
||||
url={generatePath(`${catalogLink()}/?${c.queryParams}`)}
|
||||
url={`${catalogLink()}/?${c.queryParams}`}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
@@ -112,6 +112,7 @@ export const MultistepJsonForm = ({
|
||||
widgets,
|
||||
}: Props) => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [disableButtons, setDisableButtons] = useState(false);
|
||||
|
||||
const handleReset = () => {
|
||||
setActiveStep(0);
|
||||
@@ -121,6 +122,10 @@ export const MultistepJsonForm = ({
|
||||
setActiveStep(Math.min(activeStep + 1, steps.length));
|
||||
};
|
||||
const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0));
|
||||
const handleCreate = () => {
|
||||
setDisableButtons(true);
|
||||
onFinish();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -172,9 +177,18 @@ export const MultistepJsonForm = ({
|
||||
metadata={getReviewData(formData, steps)}
|
||||
/>
|
||||
<Box mb={4} />
|
||||
<Button onClick={handleBack}>Back</Button>
|
||||
<Button onClick={handleReset}>Reset</Button>
|
||||
<Button variant="contained" color="primary" onClick={onFinish}>
|
||||
<Button onClick={handleBack} disabled={disableButtons}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={handleReset} disabled={disableButtons}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreate}
|
||||
disabled={disableButtons}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
Reference in New Issue
Block a user