Merge pull request #5282 from backstage/rugvip/config-schema
plugins: add config-schema plugin
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user