Merge branch 'master' into sonarqube-doco
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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';
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+10
-7
@@ -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 () => {
|
||||
|
||||
+10
-6
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>');
|
||||
}
|
||||
`,
|
||||
}),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user