Merge branch 'master' into sonarqube-doco

This commit is contained in:
Adam Harvey
2021-04-14 16:32:56 -04:00
committed by GitHub
72 changed files with 3776 additions and 397 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@asyncapi/react-component": "^0.19.2",
"@asyncapi/react-component": "^0.22.3",
"@backstage/catalog-model": "^0.7.5",
"@backstage/core": "^0.7.4",
"@backstage/plugin-catalog-react": "^0.1.4",
@@ -172,4 +172,35 @@ describe('readLdapConfig', () => {
];
expect(actual).toEqual(expected);
});
it('supports multiline ldap query filter', () => {
const config = {
providers: [
{
target: 'target',
users: {
dn: 'udn',
options: {
filter: `
(|
(cn=foo bar)
(cn=bar)
)
`,
},
},
groups: {
dn: 'gdn',
options: {
filter: 'f',
},
},
},
],
};
const actual = readLdapConfig(new ConfigReader(config));
const expected = '(|(cn=foo bar)(cn=bar))';
expect(actual[0].users.options.filter).toEqual(expected);
});
});
@@ -182,7 +182,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
}
return {
scope: c.getOptionalString('scope') as SearchOptions['scope'],
filter: c.getOptionalString('filter'),
filter: formatFilter(c.getOptionalString('filter')),
attributes: c.getOptionalStringArray('attributes'),
paged: c.getOptionalBoolean('paged'),
};
@@ -260,6 +260,11 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
};
}
function formatFilter(filter?: string): string | undefined {
// Remove extra whitespaces between blocks to support multiline filters from the configuration
return filter?.replace(/\s*(\(|\))/g, '$1')?.trim();
}
const providerConfigs = config.getOptionalConfigArray('providers') ?? [];
return providerConfigs.map(c => {
const newConfig = {
+1 -1
View File
@@ -42,7 +42,7 @@ export class CatalogClientWrapper implements CatalogApi {
}
async getLocationById(
id: String,
id: string,
options?: CatalogRequestOptions,
): Promise<Location | undefined> {
return await this.client.getLocationById(id, {
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+19
View File
@@ -0,0 +1,19 @@
# config-schema
The `config-schema` plugin lets you browse a documentation reference of the configuration schema of a particular Backstage installation. It is intended as a tool for integrators rather than something that is useful to end users of Backstage.
## Usage
The plugin exports a single full-page extension, the `ConfigSchemaPage`, which you add to an app like a usual top-level tool on a dedicated route.
It also exports a `configSchemaApiRef` without any default implementation, meaning that an API needs to be registered in the app for the plugin to work. An implementation of the API that is provided out of the box is the `StaticSchemaLoader`, which loads the schema from a URL. It can be added to the app by adding the following to your app's `api.ts`:
```ts
createApiFactory(configSchemaApiRef, new StaticSchemaLoader());
```
The configuration schema consumed by the `StaticSchemaLoader` can be generated using the following command:
```shell
yarn --silent backstage-cli config:schema --format=json
```
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
/*
* 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.
*/
import { createDevApp } from '@backstage/dev-utils';
import { Schema } from 'jsonschema';
import React from 'react';
import Observable from 'zen-observable';
import { configSchemaApiRef } from '../src/api';
import { ConfigSchemaResult } from '../src/api/types';
import { ConfigSchemaPage, configSchemaPlugin } from '../src/plugin';
import exampleSchema from './example-schema.json';
createDevApp()
.registerPlugin(configSchemaPlugin)
.registerApi({
api: configSchemaApiRef,
deps: {},
factory: () => ({
schema$: () =>
new Observable<ConfigSchemaResult>(sub =>
sub.next({ schema: (exampleSchema as unknown) as Schema }),
),
}),
})
.addPage({
element: <ConfigSchemaPage />,
title: 'Root Page',
})
.render();
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@backstage/plugin-config-schema",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.4",
"@backstage/errors": "^0.1.1",
"@backstage/theme": "^0.2.5",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"jsonschema": "^1.2.6",
"zen-observable": "^0.8.15",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.6.7",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"msw": "^0.21.2",
"cross-fetch": "^3.0.6"
},
"files": [
"dist"
]
}
@@ -0,0 +1,57 @@
/*
* 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.
*/
import { Observable } from '@backstage/core';
import ObservableImpl from 'zen-observable';
import { ResponseError } from '@backstage/errors';
import { Schema } from 'jsonschema';
import { ConfigSchemaApi, ConfigSchemaResult } from './types';
const DEFAULT_URL = 'config-schema.json';
/**
* A ConfigSchemaApi implementation that loads the configuration from a URL.
*/
export class StaticSchemaLoader implements ConfigSchemaApi {
private readonly url: string;
constructor({ url = DEFAULT_URL }: { url?: string } = {}) {
this.url = url;
}
schema$(): Observable<ConfigSchemaResult> {
return new ObservableImpl(subscriber => {
this.fetchSchema().then(
schema => subscriber.next({ schema }),
error => subscriber.error(error),
);
});
}
private async fetchSchema(): Promise<undefined | Schema> {
const res = await fetch(this.url);
if (!res.ok) {
if (res.status === 404) {
return undefined;
}
throw ResponseError.fromResponse(res);
}
return await res.json();
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { configSchemaApiRef } from './types';
export type { ConfigSchemaApi } from './types';
export { StaticSchemaLoader } from './StaticSchemaLoader';
+30
View File
@@ -0,0 +1,30 @@
/*
* 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.
*/
import { createApiRef, Observable } from '@backstage/core';
import { Schema } from 'jsonschema';
export interface ConfigSchemaResult {
schema?: Schema;
}
export interface ConfigSchemaApi {
schema$(): Observable<ConfigSchemaResult>;
}
export const configSchemaApiRef = createApiRef<ConfigSchemaApi>({
id: 'plugin.config-schema',
});
@@ -0,0 +1,48 @@
/*
* 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.
*/
import React, { useMemo } from 'react';
import { Header, Page, Content, useApi, Progress } from '@backstage/core';
import { useObservable } from 'react-use';
import { configSchemaApiRef } from '../../api';
import { SchemaViewer } from '../SchemaViewer';
import { Typography } from '@material-ui/core';
export const ConfigSchemaPage = () => {
const configSchemaApi = useApi(configSchemaApiRef);
const schemaResult = useObservable(
useMemo(() => configSchemaApi.schema$(), [configSchemaApi]),
);
let content;
if (schemaResult) {
if (schemaResult.schema) {
content = <SchemaViewer schema={schemaResult.schema} />;
} else {
content = (
<Typography variant="h4">No configuration schema available</Typography>
);
}
} else {
content = <Progress />;
}
return (
<Page themeId="tool">
<Header title="Configuration Reference" />
<Content stretch>{content}</Content>
</Page>
);
};
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { ConfigSchemaPage } from './ConfigSchemaPage';
@@ -0,0 +1,196 @@
/*
* 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.
*/
import { createStyles, fade, withStyles } from '@material-ui/core';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { TreeItem, TreeItemProps, TreeView } from '@material-ui/lab';
import { Schema } from 'jsonschema';
import React, { ReactNode, useMemo, useRef } from 'react';
import { useScrollTargets } from '../ScrollTargetsContext';
const StyledTreeItem = withStyles(theme =>
createStyles({
label: {
userSelect: 'none',
},
group: {
marginLeft: 7,
paddingLeft: theme.spacing(1),
borderLeft: `1px solid ${fade(theme.palette.text.primary, 0.15)}`,
},
}),
)((props: TreeItemProps) => <TreeItem {...props} />);
export function createSchemaBrowserItems(
expanded: string[],
schema: Schema,
path: string = '',
depth: number = 0,
): ReactNode {
let matchArr;
if (schema.anyOf) {
matchArr = schema.anyOf;
} else if (schema.oneOf) {
matchArr = schema.oneOf;
} else if (schema.allOf) {
matchArr = schema.allOf;
}
if (matchArr) {
return matchArr.map((childSchema, index) => {
const childPath = `${path}/${index + 1}`;
if (depth > 0) expanded.push(childPath);
return (
<StyledTreeItem
key={childPath}
nodeId={childPath}
label={`<Option ${index + 1}>`}
>
{createSchemaBrowserItems(
expanded,
childSchema,
childPath,
depth + 1,
)}
</StyledTreeItem>
);
});
}
switch (schema.type) {
case 'array': {
const childPath = `${path}[]`;
if (depth > 0) expanded.push(childPath);
return (
<StyledTreeItem nodeId={childPath} label="[]">
{schema.items &&
createSchemaBrowserItems(
expanded,
schema.items as Schema,
childPath,
depth + 1,
)}
</StyledTreeItem>
);
}
case 'object':
case undefined: {
const children = [];
if (schema.properties) {
children.push(
...Object.entries(schema.properties).map(([name, childSchema]) => {
const childPath = path ? `${path}.${name}` : name;
if (depth > 0) expanded.push(childPath);
return (
<StyledTreeItem key={childPath} nodeId={childPath} label={name}>
{createSchemaBrowserItems(
expanded,
childSchema,
childPath,
depth + 1,
)}
</StyledTreeItem>
);
}),
);
}
if (schema.patternProperties) {
children.push(
...Object.entries(schema.patternProperties).map(
([name, childSchema]) => {
const childPath = `${path}.<${name}>`;
if (depth > 0) expanded.push(childPath);
return (
<StyledTreeItem
key={childPath}
nodeId={childPath}
label={`<${name}>`}
>
{createSchemaBrowserItems(
expanded,
childSchema,
childPath,
depth + 1,
)}
</StyledTreeItem>
);
},
),
);
}
if (schema.additionalProperties && schema.additionalProperties !== true) {
const childPath = `${path}.*`;
if (depth > 0) expanded.push(childPath);
children.push(
<StyledTreeItem key={childPath} nodeId={childPath} label="*">
{createSchemaBrowserItems(
expanded,
schema.additionalProperties,
childPath,
depth + 1,
)}
</StyledTreeItem>,
);
}
return <>{children}</>;
}
default:
return null;
}
}
export function SchemaBrowser({ schema }: { schema: Schema }) {
const scroll = useScrollTargets();
const expandedRef = useRef<string[]>([]);
const data = useMemo(() => {
const expanded = new Array<string>();
const items = createSchemaBrowserItems(expanded, schema);
return { items, expanded };
}, [schema]);
if (!scroll) {
throw new Error('No scroll handler available');
}
const handleToggle = (_event: unknown, expanded: string[]) => {
expandedRef.current = expanded;
};
const handleSelect = (_event: unknown, nodeId: string) => {
if (expandedRef.current.includes(nodeId)) {
scroll.scrollTo(nodeId);
}
};
return (
<TreeView
defaultExpanded={data.expanded}
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
onNodeToggle={handleToggle}
onNodeSelect={handleSelect}
>
{data.items}
</TreeView>
);
}
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { SchemaBrowser } from './SchemaBrowser';
@@ -0,0 +1,59 @@
/*
* 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.
*/
import { Box, Typography } from '@material-ui/core';
import { Schema } from 'jsonschema';
import React from 'react';
import { ChildView } from './ChildView';
import { MetadataView } from './MetadataView';
import { SchemaViewProps } from './types';
export function ArrayView({ path, depth, schema }: SchemaViewProps) {
const itemDepth = depth + 1;
const itemPath = path ? `${path}[]` : '[]';
const itemSchema = schema.items;
return (
<>
<Box marginBottom={4}>
{schema.description && (
<Box marginBottom={4}>
<Typography variant="body1">{schema.description}</Typography>
</Box>
)}
<MetadataView schema={schema} />
</Box>
<Typography variant="overline">Items</Typography>
<ChildView
lastChild
path={itemPath}
depth={itemDepth}
schema={itemSchema as Schema | undefined}
/>
{schema.additionalItems && schema.additionalItems !== true && (
<>
<Typography variant="overline">Additional Items</Typography>
<ChildView
path={itemPath}
depth={itemDepth}
schema={schema.additionalItems}
lastChild
/>
</>
)}
</>
);
}
@@ -0,0 +1,123 @@
/*
* 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.
*/
import { JsonValue } from '@backstage/config';
import { Box, Chip, Divider, makeStyles, Typography } from '@material-ui/core';
import { Schema } from 'jsonschema';
import React, { useEffect, useRef } from 'react';
import { useScrollTargets } from '../ScrollTargetsContext/ScrollTargetsContext';
import { SchemaView } from './SchemaView';
export interface MetadataViewRowProps {
label: string;
text?: string;
data?: JsonValue;
}
function titleVariant(depth: number) {
if (depth <= 1) {
return 'h2';
} else if (depth === 2) {
return 'h3';
} else if (depth === 3) {
return 'h4';
} else if (depth === 4) {
return 'h5';
}
return 'h6';
}
const useChildViewStyles = makeStyles(theme => ({
title: {
marginBottom: 0,
},
chip: {
marginLeft: theme.spacing(1),
marginRight: 0,
marginBottom: 0,
},
}));
export function ChildView({
path,
depth,
schema,
required,
lastChild,
}: {
path: string;
depth: number;
schema?: Schema;
required?: boolean;
lastChild?: boolean;
}) {
const classes = useChildViewStyles();
const titleRef = useRef<HTMLElement>(null);
const scroll = useScrollTargets();
useEffect(() => {
return scroll?.setScrollListener(path, () => {
titleRef.current?.scrollIntoView({ behavior: 'smooth' });
});
}, [scroll, path]);
const chips = new Array<JSX.Element>();
const chipProps = { size: 'small' as const, classes: { root: classes.chip } };
if (required) {
chips.push(
<Chip label="required" color="default" key="required" {...chipProps} />,
);
}
const visibility = (schema as { visibility?: string })?.visibility;
if (visibility === 'frontend') {
chips.push(
<Chip label="frontend" color="primary" key="visibility" {...chipProps} />,
);
} else if (visibility === 'secret') {
chips.push(
<Chip label="secret" color="secondary" key="visibility" {...chipProps} />,
);
}
return (
<Box paddingBottom={lastChild ? 4 : 8} display="flex" flexDirection="row">
<Divider orientation="vertical" flexItem />
<Box paddingLeft={2} flex={1}>
<Box
display="flex"
flexDirection="row"
marginBottom={2}
alignItems="center"
>
<Typography
ref={titleRef}
variant={titleVariant(depth)}
classes={{ root: classes.title }}
>
{path}
</Typography>
{chips.length > 0 && <Box marginLeft={1} />}
{chips}
</Box>
{schema && (
<SchemaView path={path} depth={depth} schema={schema as Schema} />
)}
</Box>
</Box>
);
}
@@ -0,0 +1,46 @@
/*
* 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.
*/
import { Typography } from '@material-ui/core';
import { Schema } from 'jsonschema';
import React from 'react';
import { ChildView } from './ChildView';
export function MatchView({
path,
depth,
schema,
label,
}: {
path: string;
depth: number;
schema: Schema[];
label: string;
}) {
return (
<>
<Typography variant="overline">{label}</Typography>
{schema.map((optionSchema, index) => (
<ChildView
key={index}
path={`${path}/${index + 1}`}
depth={depth + 1}
schema={optionSchema}
lastChild={index === schema.length - 1}
/>
))}
</>
);
}
@@ -0,0 +1,110 @@
/*
* 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.
*/
import { JsonValue } from '@backstage/config';
import {
Paper,
Table,
TableBody,
TableCell,
TableRow,
Typography,
} from '@material-ui/core';
import { Schema } from 'jsonschema';
import React from 'react';
export interface MetadataViewRowProps {
label: string;
text?: string;
data?: JsonValue;
}
export function MetadataViewRow({ label, text, data }: MetadataViewRowProps) {
if (text === undefined && data === undefined) {
return null;
}
return (
<TableRow>
<TableCell style={{ width: 160 }}>
<Typography variant="body1" noWrap style={{ fontWeight: 900 }}>
{label}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body1">
{data ? JSON.stringify(data) : text}
</Typography>
</TableCell>
</TableRow>
);
}
export function MetadataView({ schema }: { schema: Schema }) {
return (
<Paper variant="outlined" square style={{ width: '100%' }}>
<Table size="small">
<TableBody>
<MetadataViewRow label="Type" data={schema.type} />
<MetadataViewRow label="Allowed values" data={schema.enum} />
{schema.additionalProperties === true && (
<MetadataViewRow label="Additional Properties" text="true" />
)}
{schema.additionalItems === true && (
<MetadataViewRow label="Additional Items" text="true" />
)}
<MetadataViewRow label="Format" text={schema.format} />
<MetadataViewRow
label="Pattern"
text={schema.pattern && String(schema.pattern)}
/>
<MetadataViewRow label="Minimum" data={schema.minimum} />
<MetadataViewRow label="Maximum" data={schema.maximum} />
<MetadataViewRow
label="Exclusive minimum"
data={schema.exclusiveMinimum}
/>
<MetadataViewRow
label="Exclusive maximum"
data={schema.exclusiveMaximum}
/>
<MetadataViewRow label="Multiple of" data={schema.multipleOf} />
<MetadataViewRow
label="Maximum number of items"
data={schema.maxItems}
/>
<MetadataViewRow
label="Minimum number of items"
data={schema.minItems}
/>
<MetadataViewRow
label="Maximum number of properties"
data={schema.maxProperties}
/>
<MetadataViewRow
label="Minimum number of properties"
data={schema.minProperties}
/>
<MetadataViewRow label="Maximum Length" data={schema.maxLength} />
<MetadataViewRow label="Minimum Length" data={schema.minLength} />
<MetadataViewRow
label="Items must be unique"
data={schema.uniqueItems}
/>
</TableBody>
</Table>
</Paper>
);
}
@@ -0,0 +1,94 @@
/*
* 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.
*/
import { Box, Typography } from '@material-ui/core';
import React from 'react';
import { ChildView } from './ChildView';
import { MetadataView } from './MetadataView';
import { SchemaViewProps } from './types';
function isRequired(name: string, required?: boolean | string[]) {
if (required === true) {
return true;
}
if (Array.isArray(required)) {
return required.includes(name);
}
return false;
}
export function ObjectView({ path, depth, schema }: SchemaViewProps) {
const properties = Object.entries(schema.properties ?? {});
const patternProperties = Object.entries(schema.patternProperties ?? {});
return (
<>
{depth > 0 && (
<Box marginBottom={4}>
{schema.description && (
<Box marginBottom={4}>
<Typography variant="body1">{schema.description}</Typography>
</Box>
)}
<MetadataView schema={schema} />
</Box>
)}
{properties.length > 0 && (
<>
{depth > 0 && <Typography variant="overline">Properties</Typography>}
{properties.map(([name, propSchema], index) => (
<ChildView
key={name}
path={path ? `${path}.${name}` : name}
depth={depth + 1}
schema={propSchema}
lastChild={index === properties.length - 1}
required={isRequired(name, schema.required)}
/>
))}
</>
)}
{patternProperties.length > 0 && (
<>
{depth > 0 && (
<Typography variant="overline">Pattern Properties</Typography>
)}
{patternProperties.map(([name, propSchema], index) => (
<ChildView
key={name}
path={path ? `${path}.<${name}>` : name}
depth={depth + 1}
schema={propSchema}
lastChild={index === patternProperties.length - 1}
required={isRequired(name, schema.required)}
/>
))}
</>
)}
{schema.additionalProperties && schema.additionalProperties !== true && (
<>
<Typography variant="overline">Additional Properties</Typography>
<ChildView
path={`${path}.*`}
depth={depth + 1}
schema={schema.additionalProperties}
lastChild
/>
</>
)}
</>
);
}
@@ -0,0 +1,33 @@
/*
* 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.
*/
import { Box, Typography } from '@material-ui/core';
import React from 'react';
import { MetadataView } from './MetadataView';
import { SchemaViewProps } from './types';
export function ScalarView({ schema }: SchemaViewProps) {
return (
<>
{schema.description && (
<Box marginBottom={4}>
<Typography variant="body1">{schema.description}</Typography>
</Box>
)}
<MetadataView schema={schema} />
</>
);
}
@@ -0,0 +1,62 @@
/*
* 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.
*/
import React from 'react';
import { ArrayView } from './ArrayView';
import { MatchView } from './MatchView';
import { ObjectView } from './ObjectView';
import { ScalarView } from './ScalarView';
import { SchemaViewProps } from './types';
export function SchemaView(props: SchemaViewProps) {
const { schema } = props;
if (schema.anyOf) {
return (
<MatchView
{...props}
schema={schema.anyOf}
label="Any of the following"
/>
);
}
if (schema.oneOf) {
return (
<MatchView
{...props}
schema={schema.oneOf}
label="One of the following"
/>
);
}
if (schema.allOf) {
return (
<MatchView
{...props}
schema={schema.allOf}
label="All of the following"
/>
);
}
switch (schema.type) {
case 'array':
return <ArrayView {...props} />;
case 'object':
case undefined:
return <ObjectView {...props} />;
default:
return <ScalarView {...props} />;
}
}
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { SchemaView } from './SchemaView';
@@ -0,0 +1,23 @@
/*
* 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.
*/
import { Schema } from 'jsonschema';
export interface SchemaViewProps {
path: string;
depth: number;
schema: Schema;
}
@@ -0,0 +1,136 @@
/*
* 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.
*/
import { render, screen } from '@testing-library/react';
import { Schema } from 'jsonschema';
import React from 'react';
import { SchemaViewer } from './SchemaViewer';
describe('SchemaViewer', () => {
it('should render a simple schema', () => {
const schema = {
type: 'object',
properties: {
a: {
description: 'My A',
type: 'string',
},
b: {
description: 'My B',
type: 'number',
},
},
};
render(<SchemaViewer schema={schema} />);
expect(screen.getAllByText('a').length).toBe(2);
expect(screen.getByText('My A')).toBeInTheDocument();
expect(screen.getAllByText('b').length).toBe(2);
expect(screen.getByText('My B')).toBeInTheDocument();
});
it('should render complex schema', () => {
const schema: Schema = {
type: 'object',
properties: {
a: {
description: 'My A',
type: 'object',
patternProperties: {
'prefix.*': {
description: 'Prefix prop',
type: 'string',
},
},
additionalProperties: {
description: 'Additional properties for A',
type: 'number',
minimum: 72,
maximum: 79,
},
},
b: {
oneOf: [
{ type: 'string', description: 'B one of 1' },
{
type: 'array',
description: 'B one of 2',
items: {
anyOf: [
{ type: 'string', description: 'Any of B 1' },
{ type: 'number', description: 'Any of B 2' },
{
type: 'object',
description: 'Any of B 3',
properties: {
deep: {
allOf: [
{
type: 'number',
description: 'Impossible 1',
},
{
type: 'boolean',
description: 'Impossible 2',
},
],
},
},
},
],
},
},
],
},
},
};
render(<SchemaViewer schema={schema} />);
expect(screen.getAllByText('a').length).toBe(2);
expect(screen.getByText('My A')).toBeInTheDocument();
expect(screen.getByText('a.<prefix.*>')).toBeInTheDocument();
expect(screen.getByText('Prefix prop')).toBeInTheDocument();
expect(screen.getByText('a.*')).toBeInTheDocument();
expect(screen.getByText('Additional properties for A')).toBeInTheDocument();
expect(screen.getByText('72')).toBeInTheDocument();
expect(screen.getByText('79')).toBeInTheDocument();
expect(screen.getAllByText('b').length).toBe(2);
expect(screen.getByText('b/1')).toBeInTheDocument();
expect(screen.getByText('B one of 1')).toBeInTheDocument();
expect(screen.getByText('b/2')).toBeInTheDocument();
expect(screen.getByText('B one of 2')).toBeInTheDocument();
expect(screen.getByText('b/2[]')).toBeInTheDocument();
expect(screen.getByText('b/2[]/1')).toBeInTheDocument();
expect(screen.getByText('Any of B 1')).toBeInTheDocument();
expect(screen.getByText('b/2[]/2')).toBeInTheDocument();
expect(screen.getByText('Any of B 2')).toBeInTheDocument();
expect(screen.getByText('b/2[]/3')).toBeInTheDocument();
expect(screen.getByText('Any of B 3')).toBeInTheDocument();
expect(screen.getByText('b/2[]/3.deep')).toBeInTheDocument();
expect(screen.getByText('b/2[]/3.deep/1')).toBeInTheDocument();
expect(screen.getByText('Impossible 1')).toBeInTheDocument();
expect(screen.getByText('b/2[]/3.deep/2')).toBeInTheDocument();
expect(screen.getByText('Impossible 2')).toBeInTheDocument();
});
});
@@ -0,0 +1,53 @@
/*
* 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.
*/
import { Box, Paper } from '@material-ui/core';
import { Schema } from 'jsonschema';
import React from 'react';
import { SchemaView } from '../SchemaView';
import { SchemaBrowser } from '../SchemaBrowser';
import { ScrollTargetsProvider } from '../ScrollTargetsContext/ScrollTargetsContext';
export interface SchemaViewerProps {
schema: Schema;
}
export const SchemaViewer = ({ schema }: SchemaViewerProps) => {
return (
<Box flex="1" position="relative">
<Box
clone
position="absolute"
display="flex"
flexDirection="row"
flexWrap="nowrap"
maxHeight="100%"
>
<Paper elevation={3}>
<ScrollTargetsProvider>
<Box padding={1} overflow="auto" width={300}>
<SchemaBrowser schema={schema} />
</Box>
<Box flex="1" overflow="auto">
<SchemaView schema={schema} path="" depth={0} />
</Box>
</ScrollTargetsProvider>
</Paper>
</Box>
</Box>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { SchemaViewer } from './SchemaViewer';
@@ -0,0 +1,52 @@
/*
* 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.
*/
import React, { createContext, ReactNode, useContext } from 'react';
class ScrollTargetsForwarder {
private readonly listeners = new Map<string, () => void>();
setScrollListener(id: string, listener: () => void): () => void {
this.listeners.set(id, listener);
return () => {
if (this.listeners.get(id) === listener) {
this.listeners.delete(id);
}
};
}
scrollTo(id: string) {
this.listeners.get(id)?.();
}
}
const ScrollTargetsContext = createContext<ScrollTargetsForwarder | undefined>(
undefined,
);
export function ScrollTargetsProvider({ children }: { children: ReactNode }) {
return (
<ScrollTargetsContext.Provider
value={new ScrollTargetsForwarder()}
children={children}
/>
);
}
export function useScrollTargets() {
return useContext(ScrollTargetsContext);
}
@@ -0,0 +1,20 @@
/*
* 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.
*/
export {
ScrollTargetsProvider,
useScrollTargets,
} from './ScrollTargetsContext';
+18
View File
@@ -0,0 +1,18 @@
/*
* 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.
*/
export * from './api';
export { configSchemaPlugin, ConfigSchemaPage } from './plugin';
+22
View File
@@ -0,0 +1,22 @@
/*
* 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.
*/
import { configSchemaPlugin } from './plugin';
describe('config-schema', () => {
it('should export plugin', () => {
expect(configSchemaPlugin).toBeDefined();
});
});
+33
View File
@@ -0,0 +1,33 @@
/*
* 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.
*/
import { createPlugin, createRoutableExtension } from '@backstage/core';
import { rootRouteRef } from './routes';
export const configSchemaPlugin = createPlugin({
id: 'config-schema',
routes: {
root: rootRouteRef,
},
});
export const ConfigSchemaPage = configSchemaPlugin.provide(
createRoutableExtension({
component: () =>
import('./components/ConfigSchemaPage').then(m => m.ConfigSchemaPage),
mountPoint: rootRouteRef,
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* 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.
*/
import { createRouteRef } from '@backstage/core';
export const rootRouteRef = createRouteRef({
title: 'config-schema',
});
+17
View File
@@ -0,0 +1,17 @@
/*
* 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.
*/
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
@@ -21,7 +21,7 @@ import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator
export class KubernetesAuthTranslatorGenerator {
static getKubernetesAuthTranslatorInstance(
authProvider: String,
authProvider: string,
): KubernetesAuthTranslator {
switch (authProvider) {
case 'google': {
@@ -34,8 +34,8 @@ const mockFetch = (mock: jest.Mock) => {
};
function generateMockResourcesAndErrors(
serviceId: String,
clusterName: String,
serviceId: string,
clusterName: string,
) {
if (clusterName === 'empty-cluster') {
return {
@@ -16,6 +16,8 @@
import mockFs from 'mock-fs';
import { Writable } from 'stream';
import os from 'os';
import { resolve as resolvePath } from 'path';
import {
PullRequestCreator,
GithubPullRequestActionInput,
@@ -28,7 +30,8 @@ import { getRootLogger } from '@backstage/backend-common';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
const id = 'createPublishGithubPullRequestAction';
const root = os.platform() === 'win32' ? 'C:\\root' : '/root';
const workspacePath = resolvePath(root, 'my-workspace');
describe('createPublishGithubPullRequestAction', () => {
let instance: TemplateAction<GithubPullRequestActionInput>;
@@ -72,7 +75,7 @@ describe('createPublishGithubPullRequestAction', () => {
};
mockFs({
[id]: { 'file.txt': 'Hello there!' },
[workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = {
@@ -81,7 +84,7 @@ describe('createPublishGithubPullRequestAction', () => {
logger: getRootLogger(),
logStream: new Writable(),
input,
workspacePath: id,
workspacePath,
};
});
it('creates a pull request', async () => {
@@ -133,7 +136,7 @@ describe('createPublishGithubPullRequestAction', () => {
};
mockFs({
[id]: {
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
@@ -145,7 +148,7 @@ describe('createPublishGithubPullRequestAction', () => {
logger: getRootLogger(),
logStream: new Writable(),
input,
workspacePath: id,
workspacePath,
};
});
@@ -188,7 +191,7 @@ describe('createPublishGithubPullRequestAction', () => {
};
mockFs({
[id]: { 'file.txt': 'Hello there!' },
[workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = {
@@ -197,7 +200,7 @@ describe('createPublishGithubPullRequestAction', () => {
logger: getRootLogger(),
logStream: new Writable(),
input,
workspacePath: id,
workspacePath,
};
});
it('creates a pull request', async () => {
@@ -197,17 +197,21 @@ export const createPublishGithubPullRequestAction = ({
const client = await clientFactory({ integrations, host, owner, repo });
const fileRoot = sourcePath
? path.join(ctx.workspacePath, sourcePath)
? path.resolve(ctx.workspacePath, sourcePath)
: ctx.workspacePath;
const localFilePaths = await globby(`${fileRoot}/**/*.*`);
const localFilePaths = await globby(['./**', './**/.*', '!.git'], {
cwd: fileRoot,
gitignore: true,
dot: true,
});
const fileContents = await Promise.all(
localFilePaths.map(p => readFile(p)),
localFilePaths.map(p => readFile(path.resolve(fileRoot, p))),
);
const repoFilePaths = localFilePaths.map(p => {
const relativePath = path.relative(fileRoot, p);
return targetPath ? `${targetPath}/${relativePath}` : relativePath;
const repoFilePaths = localFilePaths.map(repoFilePath => {
return targetPath ? `${targetPath}/${repoFilePath}` : repoFilePath;
});
const changes = [
+14 -3
View File
@@ -67,8 +67,12 @@ proxy:
allowedMethods: ['GET']
headers:
Authorization:
# Content: 'Basic base64("<api-key>:")' <-- note the trailing ':'
# Example: Basic bXktYXBpLWtleTo=
# Environmental variable: SONARQUBE_AUTH_HEADER
# Value: 'Basic base64("<sonar-auth-token>:")'
# Encode the "<sonar-auth-token>:" string using base64 encoder.
# Note the trailing colon (:) at the end of the token.
# Example environmental config: SONARQUBE_AUTH_HEADER=Basic bXktYXBpLWtleTo=
# Fetch the sonar-auth-token from https://sonarcloud.io/account/security/
$env: SONARQUBE_AUTH_HEADER
sonarQube:
@@ -77,7 +81,14 @@ sonarQube:
5. Get and provide `SONARQUBE_AUTH_HEADER` as env variable (https://sonarcloud.io/account/security or https://docs.sonarqube.org/latest/user-guide/user-token/)
6. Add the `sonarqube.org/project-key` annotation to your entity's `catalog-info.yaml` file:
6. Run the following commands in the root folder of the project to install and compile the changes.
```yaml
yarn install
yarn tsc
```
7. Add the `sonarqube.org/project-key` annotation to the `catalog-info.yaml` file of the target repo for which code quality analysis is needed.
```yaml
apiVersion: backstage.io/v1alpha1
@@ -19,6 +19,7 @@ import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import { Button, DialogActions, DialogContent } from '@material-ui/core';
import LinkIcon from '@material-ui/icons/Link';
import { MarkdownContent } from '@backstage/core';
export type Props = {
open: boolean;
@@ -43,7 +44,9 @@ const RadarDescription = (props: Props): JSX.Element => {
<DialogTitle data-testid="radar-description-dialog-title">
{title}
</DialogTitle>
<DialogContent dividers>{description}</DialogContent>
<DialogContent dividers>
<MarkdownContent content={description} />
</DialogContent>
{url && (
<DialogActions>
<Button
+1 -1
View File
@@ -50,7 +50,7 @@ entries.push({
title: 'JavaScript',
quadrant: 'languages',
description:
'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum',
'Excepteur **sint** occaecat *cupidatat* non proident, sunt in culpa qui officia deserunt mollit anim id est laborum',
});
entries.push({
timeline: [
@@ -14,7 +14,13 @@
* limitations under the License.
*/
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
ApiProvider,
ApiRegistry,
ConfigApi,
configApiRef,
ConfigReader,
} from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
@@ -26,7 +32,16 @@ describe('TechDocs Home', () => {
getEntities: async () => ({ items: [] }),
};
const apiRegistry = ApiRegistry.with(catalogApiRef, catalogApi);
const configApi: ConfigApi = new ConfigReader({
organization: {
name: 'My Company',
},
});
const apiRegistry = ApiRegistry.with(catalogApiRef, catalogApi).with(
configApiRef,
configApi,
);
it('should render a TechDocs home page', async () => {
await renderInTestApp(
@@ -38,7 +53,7 @@ describe('TechDocs Home', () => {
// Header
expect(await screen.findByText('Documentation')).toBeInTheDocument();
expect(
await screen.findByText(/Documentation available in Backstage/i),
await screen.findByText(/Documentation available in My Company/i),
).toBeInTheDocument();
// Explore Content
@@ -21,6 +21,8 @@ import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
import {
CodeSnippet,
ConfigApi,
configApiRef,
Content,
Header,
HeaderTabs,
@@ -36,6 +38,7 @@ import { OwnedContent } from './OwnedContent';
export const TechDocsHome = () => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const catalogApi: CatalogApi = useApi(catalogApiRef);
const configApi: ConfigApi = useApi(configApiRef);
const tabs = [{ label: 'Overview' }, { label: 'Owned Documents' }];
@@ -46,13 +49,14 @@ export const TechDocsHome = () => {
});
});
const generatedSubtitle = `Documentation available in ${
configApi.getOptionalString('organization.name') ?? 'Backstage'
}`;
if (loading) {
return (
<Page themeId="documentation">
<Header
title="Documentation"
subtitle="Documentation available in Backstage"
/>
<Header title="Documentation" subtitle={generatedSubtitle} />
<Content>
<Progress />
</Content>
@@ -63,10 +67,7 @@ export const TechDocsHome = () => {
if (error) {
return (
<Page themeId="documentation">
<Header
title="Documentation"
subtitle="Documentation available in Backstage"
/>
<Header title="Documentation" subtitle={generatedSubtitle} />
<Content>
<WarningPanel
severity="error"
@@ -81,10 +82,7 @@ export const TechDocsHome = () => {
return (
<Page themeId="documentation">
<Header
title="Documentation"
subtitle="Documentation available in Backstage"
/>
<Header title="Documentation" subtitle={generatedSubtitle} />
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
@@ -162,6 +162,16 @@ export const Reader = ({ entityId, onReady }: Props) => {
.md-typeset { font-size: 1rem; }
.md-nav { font-size: 1rem; }
.md-grid { max-width: 90vw; margin: 0 }
.md-typeset table:not([class]) {
font-size: 1rem;
border: 1px solid ${theme.palette.text.primary};
border-bottom: none;
border-collapse: collapse;
}
.md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
border-bottom: 1px solid ${theme.palette.text.primary};
}
.md-typeset table:not([class]) th { font-weight: bold; }
@media screen and (max-width: 76.1875em) {
.md-nav {
background-color: ${theme.palette.background.default};
@@ -221,6 +231,7 @@ export const Reader = ({ entityId, onReady }: Props) => {
:host {
--md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z"/></svg>');
--md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2m-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
}
`,
}),
]);