Skip to content

Commit 0e7ac17

Browse files
authored
Merge pull request #20 from smart-data-lake/release/1.1.1
Release/1.1.1
2 parents 105fcf9 + e3061da commit 0e7ac17

8 files changed

+152
-35
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sdlb-schema-viewer",
33
"description": "React component for visualizing the Smart Data Lake Builder config schema.",
4-
"version": "1.1.0",
4+
"version": "1.1.1",
55
"main": "./dist/main.js",
66
"peerDependencies": {
77
"@emotion/react": "11.x",

src/components/DetailsPanelContent.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default function DetailsPanelContent(props: { node: SchemaNode, createNod
1414
<Box sx={{flex: 1, paddingLeft: 2, paddingRight: 2, overflow: 'auto'}}>
1515
<Box sx={{display: 'flex', justifyContent: 'space-between', marginTop: 3, alignItems: 'center'}}>
1616
<Typography level="h5" sx={{wordBreak: 'break-word'}}>{nodeName}</Typography>
17-
<ShareButton onClick={() => navigator.clipboard.writeText(props.createNodeUrl(props.node))} />
17+
<ShareButton getNodeUrl={() => props.createNodeUrl(props.node)} />
1818
</Box>
1919
{deprecated && <Typography sx={{fontStyle: 'italic'}}>deprecated</Typography>}
2020
<SectionDivider />
@@ -27,17 +27,6 @@ export default function DetailsPanelContent(props: { node: SchemaNode, createNod
2727
);
2828
}
2929

30-
function ShareButton(props: { onClick: () => void }) {
31-
return (
32-
<Tooltip title="Copy link to schema element">
33-
<IconButton onClick={props.onClick} variant="plain" size="sm">
34-
<Share />
35-
</IconButton>
36-
</Tooltip>
37-
);
38-
}
39-
40-
4130
const SectionDivider = styled(Divider)({
4231
marginTop: 24,
4332
marginBottom: 24
@@ -52,6 +41,24 @@ const SectionText = (props: { children?: string }) => {
5241
<Typography level="body2" sx={{wordBreak: 'break-word', whiteSpace: 'pre-wrap'}}>{props.children}</Typography>);
5342
}
5443

44+
function ShareButton(props: { getNodeUrl: () => string }) {
45+
return (
46+
<Tooltip title="Copy link to schema element">
47+
<IconButton onClick={() => writeToClipboard(props.getNodeUrl())} variant="plain" size="sm">
48+
<Share />
49+
</IconButton>
50+
</Tooltip>
51+
);
52+
}
53+
54+
function writeToClipboard(text: string): void {
55+
if (!navigator.clipboard) {
56+
alert('Cannot copy to clipboard. Make sure you use a secure connection.');
57+
return;
58+
}
59+
navigator.clipboard.writeText(text);
60+
}
61+
5562
const nodeNameVisitor: SchemaVisitor<string> = {
5663
visitClassNode: (n: ClassNode) => n.className,
5764
visitPropertyNode: (n: PropertyNode) => n.propertyName,

src/components/DownloadButton.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { Download } from '@mui/icons-material';
2-
import { IconButton } from '@mui/joy';
2+
import { IconButton, Tooltip } from '@mui/joy';
33
import React from 'react';
44
import { headerButtonSize, headerButtonStyle } from './SchemaViewerHeader';
55

66
export default function DownloadButton(props: { toDownload: Blob | null, fileName: string | undefined }) {
77

88
return (
9-
<IconButton
10-
component="a"
11-
disabled={!props.toDownload}
12-
href={props.toDownload ? URL.createObjectURL(props.toDownload) : ''}
13-
download={props.fileName}
14-
size={headerButtonSize}
15-
sx={headerButtonStyle}>
16-
<Download />
17-
</IconButton>
9+
<Tooltip title="Download schema file">
10+
<IconButton
11+
component="a"
12+
disabled={!props.toDownload}
13+
href={props.toDownload ? URL.createObjectURL(props.toDownload) : ''}
14+
download={props.fileName}
15+
size={headerButtonSize}
16+
sx={headerButtonStyle}>
17+
<Download />
18+
</IconButton>
19+
</Tooltip>
1820
);
1921
}

src/components/SchemaGraph.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export default function SchemaGraph(props: {
6060
);
6161
}
6262

63-
function haveSchemaTreeColorsChanged(currentColors: SchemaTreeColors, nextColors: SchemaTreeColors) {
63+
function haveSchemaTreeColorsChanged(currentColors: SchemaTreeColors, nextColors: SchemaTreeColors): boolean {
6464
return JSON.stringify(currentColors) !== JSON.stringify(nextColors);
6565
}
6666

src/components/SchemaViewer.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ test('example schema is loaded and rendered without errors', async () => {
88
<SchemaViewer
99
loadSchemaNames={() => Promise.resolve(['schema1.json', 'schema2.json'])}
1010
loadSchema={schemaName => {
11+
// due to the sorting order, schema2.json should be preselected
1112
expect(schemaName).toBe('schema2.json');
1213
return Promise.resolve(exampleSchema);
1314
}} />
@@ -18,4 +19,6 @@ test('example schema is loaded and rendered without errors', async () => {
1819
expect(screen.getByText('connections[mapOf]')).toBeInTheDocument();
1920
expect(screen.getByText('dataObjects[mapOf]*')).toBeInTheDocument();
2021
expect(screen.getByText('actions[mapOf]*')).toBeInTheDocument();
22+
expect(screen.getAllByText('schema2.json').length).toBe(2); // one as the selected schema and one in dropdown
23+
expect(screen.getAllByText('schema1.json').length).toBe(1);
2124
});

src/utils/D3SchemaTree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const heightPerNode = 35;
1212
// constants for the spacing between the levels of the tree
1313
const minLabelSpace = 30;
1414
const labelSpaceFactor = 8;
15-
const labelSpaceOffset = 50;
15+
const labelSpaceOffset = 80;
1616

1717
const selectedClass = 'selected';
1818

src/utils/JsonSchemaParser.test.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ describe('schema references are resolved in', () => {
259259
expect(classNode2!.baseClass).toBe('BaseClass');
260260
});
261261

262-
test('array', () => {
262+
test('array with single ref', () => {
263263
let jsonSchema = {
264264
"type": "object",
265265
"properties": {
@@ -288,6 +288,98 @@ describe('schema references are resolved in', () => {
288288
expect(classNode.className).toBe('ClassName');
289289
expect(classNode.baseClass).toBe('BaseClass');
290290
});
291+
292+
test.each([['oneOf'], ['allOf'], ['anyOf']])('array containing %s', (type: string) => {
293+
let jsonSchema = {
294+
"type": "object",
295+
"properties": {
296+
"property": {
297+
"type": "array",
298+
"items": {
299+
[type]: [{
300+
"$ref": "#/definitions/BaseClass/ConcreteClass1"
301+
}, {
302+
"$ref": "#/definitions/BaseClass/ConcreteClass2"
303+
}]
304+
}
305+
}
306+
},
307+
"definitions": {
308+
"BaseClass": {
309+
"ConcreteClass1": {
310+
"type": "object",
311+
"title": "ClassName1",
312+
"description": "some description"
313+
},
314+
"ConcreteClass2": {
315+
"type": "object",
316+
"title": "ClassName2",
317+
"deprecated": true,
318+
}
319+
}
320+
}
321+
}
322+
const root = new JsonSchemaParser(jsonSchema as JSONSchema).parseSchema();
323+
324+
const propertyNode = root.children[0] as PropertyNode;
325+
const classNode1 = propertyNode.children.map(c => c as ClassNode).find(c => c.className === 'ClassName1')
326+
const classNode2 = propertyNode.children.map(c => c as ClassNode).find(c => c.className === 'ClassName2')
327+
expect(propertyNode.type).toBe("array");
328+
expect(propertyNode.typeDetails).toBe(`${type} BaseClass`);
329+
expect(classNode1!.baseClass).toBe('BaseClass');
330+
expect(classNode1!.description).toBe('some description');
331+
expect(classNode1!.deprecated).toBe(false);
332+
expect(classNode2!.baseClass).toBe('BaseClass');
333+
expect(classNode2!.description).toBeUndefined();
334+
expect(classNode2!.deprecated).toBe(true);
335+
});
336+
337+
test('array containing mapOf', () => {
338+
let jsonSchema = {
339+
"type": "object",
340+
"properties": {
341+
"property": {
342+
"type": "array",
343+
"items": {
344+
"additionalProperties": {
345+
"oneOf": [{
346+
"$ref": "#/definitions/BaseClass/ConcreteClass1"
347+
}, {
348+
"$ref": "#/definitions/BaseClass/ConcreteClass2"
349+
}]
350+
}
351+
}
352+
}
353+
},
354+
"definitions": {
355+
"BaseClass": {
356+
"ConcreteClass1": {
357+
"type": "object",
358+
"title": "ClassName1",
359+
"description": "some description"
360+
},
361+
"ConcreteClass2": {
362+
"type": "object",
363+
"title": "ClassName2",
364+
"deprecated": true,
365+
}
366+
}
367+
}
368+
}
369+
const root = new JsonSchemaParser(jsonSchema as JSONSchema).parseSchema();
370+
371+
const propertyNode = root.children[0] as PropertyNode;
372+
const classNode1 = propertyNode.children.map(c => c as ClassNode).find(c => c.className === 'ClassName1')
373+
const classNode2 = propertyNode.children.map(c => c as ClassNode).find(c => c.className === 'ClassName2')
374+
expect(propertyNode.type).toBe("array");
375+
expect(propertyNode.typeDetails).toBe(`mapOf BaseClass`);
376+
expect(classNode1!.baseClass).toBe('BaseClass');
377+
expect(classNode1!.description).toBe('some description');
378+
expect(classNode1!.deprecated).toBe(false);
379+
expect(classNode2!.baseClass).toBe('BaseClass');
380+
expect(classNode2!.description).toBeUndefined();
381+
expect(classNode2!.deprecated).toBe(true);
382+
});
291383
});
292384

293385
test('base class is undefined if schema is defined in "Others" section', () => {

src/utils/JsonSchemaParser.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { JSONSchema7 } from 'json-schema';
33

44
export type JSONSchema = JSONSchema7;
55

6+
// types which are used when a class can be chosen from a list of classes
7+
const classSelectionTypes: SchemaType[] = ['anyOf', 'allOf', 'oneOf', 'mapOf'];
8+
69
/**
710
* For constructing a {@link SchemaNode} tree from a JSON schema.
811
*/
@@ -40,8 +43,9 @@ export default class JsonSchemaParser {
4043
let {type, typeDetails} = this.parseType(resolvedPropertySchema);
4144
const deprecated = this.isDeprecated(propertySchema);
4245
const childNodes = this.parseChildren(type, resolvedPropertySchema);
43-
if (!typeDetails && this.hasClassNodeChildren(type, propertySchema)) {
44-
typeDetails = this.inferTypeDetailsFromChildClasses(childNodes as ClassNode[]);
46+
if (this.hasClassNodeChildren(type, propertySchema)) {
47+
const typeDetailsAboutChildren = this.inferTypeDetailsFromChildClasses(childNodes as ClassNode[]);
48+
typeDetails = typeDetails ? `${typeDetails} ${typeDetailsAboutChildren}` : typeDetailsAboutChildren;
4549
}
4650
const propertyNode = new PropertyNode(this.idGenerator.generateId(), name, type, required, deprecated, typeDetails,
4751
resolvedPropertySchema.description);
@@ -113,15 +117,15 @@ export default class JsonSchemaParser {
113117
}
114118

115119
private hasClassNodeChildren(type: SchemaType, propertySchema: JSONSchema): boolean {
116-
return ['anyOf', 'allOf', 'oneOf', 'mapOf'].includes(type)
117-
|| (type === 'array' && this.getArrayItemType(propertySchema) === 'object');
120+
return classSelectionTypes.includes(type)
121+
|| (type === 'array' && [...classSelectionTypes, 'object'].includes(this.getArrayItemType(propertySchema)));
118122
}
119123

120124
private getArrayItemType(array: JSONSchema): SchemaType {
121-
// we only have single object items in our schema
125+
// we only have single object items in our schema, so it is not an array
122126
const items = array.items as JSONSchema;
123127
const resolvedItems = items.$ref ? this.getSchemaFromRef(items.$ref) : items;
124-
return resolvedItems.type as SchemaType;
128+
return this.parseType(resolvedItems).type;
125129
}
126130

127131
private getClassElements(type: SchemaType, schemaElement: JSONSchema): JSONSchema[] {
@@ -137,13 +141,22 @@ export default class JsonSchemaParser {
137141
case 'mapOf':
138142
return (schemaElement.additionalProperties as JSONSchema).oneOf as JSONSchema[];
139143
case 'array':
140-
// we only have single items in our schema
141-
return schemaElement.items ? [schemaElement.items as JSONSchema] : [];
144+
return this.getClassElementsForArray(schemaElement);
142145
default:
143146
throw new Error(`Type ${type} does not have class node children.`)
144147
}
145148
}
146149

150+
private getClassElementsForArray(arrayElement: JSONSchema): JSONSchema[] {
151+
if (!arrayElement.items) {
152+
return [];
153+
}
154+
// we only have single object items in our schema, so it is not an array
155+
const items = arrayElement.items as JSONSchema;
156+
const itemsType = this.getArrayItemType(arrayElement);
157+
return classSelectionTypes.includes(itemsType) ? this.getClassElements(itemsType, items) : [items];
158+
}
159+
147160
private parseClass(classSchema: JSONSchema): ClassNode {
148161
const resolvedClassSchema = classSchema.$ref ? this.getSchemaFromRef(classSchema.$ref) : classSchema;
149162
const baseClass = classSchema.$ref ? this.extractBaseClassFromRef(classSchema.$ref) : undefined;
@@ -192,4 +205,4 @@ class NodeIdGenerator {
192205
generateId(): number {
193206
return this.nextId++;
194207
}
195-
}
208+
}

0 commit comments

Comments
 (0)