@jpmorganchase/elemental 1.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__fixtures__/api-descriptions/Instagram.d.ts +1547 -0
- package/__fixtures__/api-descriptions/badgesForSchema.d.ts +1 -0
- package/__fixtures__/api-descriptions/simpleApiWithInternalOperations.d.ts +224 -0
- package/__fixtures__/api-descriptions/simpleApiWithoutDescription.d.ts +212 -0
- package/__fixtures__/api-descriptions/todosApiBundled.d.ts +1 -0
- package/__fixtures__/api-descriptions/zoomApiYaml.d.ts +1 -0
- package/components/API/APIWithSidebarLayout.d.ts +17 -0
- package/components/API/APIWithStackedLayout.d.ts +15 -0
- package/components/API/utils.d.ts +18 -0
- package/containers/API.d.ts +23 -0
- package/containers/API.spec.d.ts +3 -0
- package/containers/API.stories.d.ts +57 -0
- package/hooks/useExportDocumentProps.d.ts +11 -0
- package/hooks/useExportDocumentProps.spec.d.ts +1 -0
- package/index.esm.js +516 -0
- package/index.js +549 -0
- package/index.mjs +516 -0
- package/package.json +17 -76
- package/styles.min.css +1 -0
- package/utils/oas/index.d.ts +3 -0
- package/utils/oas/oas2.d.ts +2 -0
- package/utils/oas/oas3.d.ts +2 -0
- package/utils/oas/types.d.ts +30 -0
- package/web-components/components.d.ts +1 -0
- package/web-components/index.d.ts +1 -0
- package/web-components.min.js +2 -0
- package/web-components.min.js.LICENSE.txt +189 -0
- package/.storybook/main.js +0 -1
- package/.storybook/manager.js +0 -1
- package/.storybook/preview.jsx +0 -3
- package/jest.config.js +0 -7
- package/src/__fixtures__/api-descriptions/Instagram.ts +0 -1859
- package/src/__fixtures__/api-descriptions/badgesForSchema.ts +0 -36
- package/src/__fixtures__/api-descriptions/simpleApiWithInternalOperations.ts +0 -253
- package/src/__fixtures__/api-descriptions/simpleApiWithoutDescription.ts +0 -243
- package/src/__fixtures__/api-descriptions/todosApiBundled.ts +0 -430
- package/src/__fixtures__/api-descriptions/zoomApiYaml.ts +0 -6083
- package/src/components/API/APIWithSidebarLayout.tsx +0 -111
- package/src/components/API/APIWithStackedLayout.tsx +0 -220
- package/src/components/API/__tests__/utils.test.ts +0 -848
- package/src/components/API/utils.ts +0 -174
- package/src/containers/API.spec.tsx +0 -131
- package/src/containers/API.stories.tsx +0 -99
- package/src/containers/API.tsx +0 -200
- package/src/hooks/useExportDocumentProps.spec.tsx +0 -68
- package/src/hooks/useExportDocumentProps.tsx +0 -48
- package/src/styles.css +0 -1
- package/src/utils/oas/__tests__/oas.spec.ts +0 -272
- package/src/utils/oas/index.ts +0 -150
- package/src/utils/oas/oas2.ts +0 -31
- package/src/utils/oas/oas3.ts +0 -37
- package/src/utils/oas/types.ts +0 -31
- package/src/web-components/__stories__/Api.stories.tsx +0 -63
- package/src/web-components/components.ts +0 -20
- package/src/web-components/index.ts +0 -3
- package/tsconfig.build.json +0 -18
- package/tsconfig.json +0 -7
- package/web-components.config.js +0 -1
- /package/{src/index.ts → index.d.ts} +0 -0
@@ -1,174 +0,0 @@
|
|
1
|
-
import { isHttpOperation, isHttpService, TableOfContentsItem } from '@stoplight/elements-core';
|
2
|
-
import { NodeType } from '@stoplight/types';
|
3
|
-
import { defaults } from 'lodash';
|
4
|
-
|
5
|
-
import { OperationNode, ServiceChildNode, ServiceNode } from '../../utils/oas/types';
|
6
|
-
|
7
|
-
export type TagGroup = { title: string; items: OperationNode[] };
|
8
|
-
|
9
|
-
export const computeTagGroups = (serviceNode: ServiceNode) => {
|
10
|
-
const groupsByTagId: { [tagId: string]: TagGroup } = {};
|
11
|
-
const ungrouped = [];
|
12
|
-
|
13
|
-
const lowerCaseServiceTags = serviceNode.tags.map(tn => tn.toLowerCase());
|
14
|
-
|
15
|
-
for (const node of serviceNode.children) {
|
16
|
-
if (node.type !== NodeType.HttpOperation) continue;
|
17
|
-
const tagName = node.tags[0];
|
18
|
-
|
19
|
-
if (tagName) {
|
20
|
-
const tagId = tagName.toLowerCase();
|
21
|
-
if (groupsByTagId[tagId]) {
|
22
|
-
groupsByTagId[tagId].items.push(node);
|
23
|
-
} else {
|
24
|
-
const serviceTagIndex = lowerCaseServiceTags.findIndex(tn => tn === tagId);
|
25
|
-
const serviceTagName = serviceNode.tags[serviceTagIndex];
|
26
|
-
groupsByTagId[tagId] = {
|
27
|
-
title: serviceTagName || tagName,
|
28
|
-
items: [node],
|
29
|
-
};
|
30
|
-
}
|
31
|
-
} else {
|
32
|
-
ungrouped.push(node);
|
33
|
-
}
|
34
|
-
}
|
35
|
-
|
36
|
-
const orderedTagGroups = Object.entries(groupsByTagId)
|
37
|
-
.sort(([g1], [g2]) => {
|
38
|
-
const g1LC = g1.toLowerCase();
|
39
|
-
const g2LC = g2.toLowerCase();
|
40
|
-
const g1Idx = lowerCaseServiceTags.findIndex(tn => tn === g1LC);
|
41
|
-
const g2Idx = lowerCaseServiceTags.findIndex(tn => tn === g2LC);
|
42
|
-
|
43
|
-
// Move not-tagged groups to the bottom
|
44
|
-
if (g1Idx < 0 && g2Idx < 0) return 0;
|
45
|
-
if (g1Idx < 0) return 1;
|
46
|
-
if (g2Idx < 0) return -1;
|
47
|
-
|
48
|
-
// sort tagged groups according to the order found in HttpService
|
49
|
-
return g1Idx - g2Idx;
|
50
|
-
})
|
51
|
-
.map(([, tagGroup]) => tagGroup);
|
52
|
-
|
53
|
-
return { groups: orderedTagGroups, ungrouped };
|
54
|
-
};
|
55
|
-
|
56
|
-
interface ComputeAPITreeConfig {
|
57
|
-
hideSchemas?: boolean;
|
58
|
-
hideInternal?: boolean;
|
59
|
-
}
|
60
|
-
|
61
|
-
const defaultComputerAPITreeConfig = {
|
62
|
-
hideSchemas: false,
|
63
|
-
hideInternal: false,
|
64
|
-
};
|
65
|
-
|
66
|
-
export const computeAPITree = (serviceNode: ServiceNode, config: ComputeAPITreeConfig = {}) => {
|
67
|
-
const mergedConfig = defaults(config, defaultComputerAPITreeConfig);
|
68
|
-
const tree: TableOfContentsItem[] = [];
|
69
|
-
|
70
|
-
tree.push({
|
71
|
-
id: '/',
|
72
|
-
slug: '/',
|
73
|
-
title: 'Overview',
|
74
|
-
type: 'overview',
|
75
|
-
meta: '',
|
76
|
-
});
|
77
|
-
|
78
|
-
const operationNodes = serviceNode.children.filter(node => node.type === NodeType.HttpOperation);
|
79
|
-
if (operationNodes.length) {
|
80
|
-
tree.push({
|
81
|
-
title: 'Endpoints',
|
82
|
-
});
|
83
|
-
|
84
|
-
const { groups, ungrouped } = computeTagGroups(serviceNode);
|
85
|
-
|
86
|
-
// Show ungroupped operations above tag groups
|
87
|
-
ungrouped.forEach(operationNode => {
|
88
|
-
if (mergedConfig.hideInternal && operationNode.data.internal) {
|
89
|
-
return;
|
90
|
-
}
|
91
|
-
tree.push({
|
92
|
-
id: operationNode.uri,
|
93
|
-
slug: operationNode.uri,
|
94
|
-
title: operationNode.name,
|
95
|
-
type: operationNode.type,
|
96
|
-
meta: operationNode.data.method,
|
97
|
-
});
|
98
|
-
});
|
99
|
-
|
100
|
-
groups.forEach(group => {
|
101
|
-
const items = group.items.flatMap(operationNode => {
|
102
|
-
if (mergedConfig.hideInternal && operationNode.data.internal) {
|
103
|
-
return [];
|
104
|
-
}
|
105
|
-
return {
|
106
|
-
id: operationNode.uri,
|
107
|
-
slug: operationNode.uri,
|
108
|
-
title: operationNode.name,
|
109
|
-
type: operationNode.type,
|
110
|
-
meta: operationNode.data.method,
|
111
|
-
};
|
112
|
-
});
|
113
|
-
if (items.length > 0) {
|
114
|
-
tree.push({
|
115
|
-
title: group.title,
|
116
|
-
items,
|
117
|
-
});
|
118
|
-
}
|
119
|
-
});
|
120
|
-
}
|
121
|
-
|
122
|
-
let schemaNodes = serviceNode.children.filter(node => node.type === NodeType.Model);
|
123
|
-
if (mergedConfig.hideInternal) {
|
124
|
-
schemaNodes = schemaNodes.filter(node => !node.data['x-internal']);
|
125
|
-
}
|
126
|
-
|
127
|
-
if (!mergedConfig.hideSchemas && schemaNodes.length) {
|
128
|
-
tree.push({
|
129
|
-
title: 'Schemas',
|
130
|
-
});
|
131
|
-
|
132
|
-
schemaNodes.forEach(node => {
|
133
|
-
tree.push({
|
134
|
-
id: node.uri,
|
135
|
-
slug: node.uri,
|
136
|
-
title: node.name,
|
137
|
-
type: node.type,
|
138
|
-
meta: '',
|
139
|
-
});
|
140
|
-
});
|
141
|
-
}
|
142
|
-
return tree;
|
143
|
-
};
|
144
|
-
|
145
|
-
export const findFirstNodeSlug = (tree: TableOfContentsItem[]): string | void => {
|
146
|
-
for (const item of tree) {
|
147
|
-
if ('slug' in item) {
|
148
|
-
return item.slug;
|
149
|
-
}
|
150
|
-
|
151
|
-
if ('items' in item) {
|
152
|
-
const slug = findFirstNodeSlug(item.items);
|
153
|
-
if (slug) {
|
154
|
-
return slug;
|
155
|
-
}
|
156
|
-
}
|
157
|
-
}
|
158
|
-
|
159
|
-
return;
|
160
|
-
};
|
161
|
-
|
162
|
-
export const isInternal = (node: ServiceChildNode | ServiceNode): boolean => {
|
163
|
-
const data = node.data;
|
164
|
-
|
165
|
-
if (isHttpOperation(data)) {
|
166
|
-
return !!data.internal;
|
167
|
-
}
|
168
|
-
|
169
|
-
if (isHttpService(data)) {
|
170
|
-
return false;
|
171
|
-
}
|
172
|
-
|
173
|
-
return !!data['x-internal'];
|
174
|
-
};
|
@@ -1,131 +0,0 @@
|
|
1
|
-
import '@testing-library/jest-dom';
|
2
|
-
|
3
|
-
import {
|
4
|
-
withMosaicProvider,
|
5
|
-
withPersistenceBoundary,
|
6
|
-
withQueryClientProvider,
|
7
|
-
withStyles,
|
8
|
-
} from '@stoplight/elements-core';
|
9
|
-
import { render, screen } from '@testing-library/react';
|
10
|
-
import { createMemoryHistory } from 'history';
|
11
|
-
import { flow } from 'lodash';
|
12
|
-
import * as React from 'react';
|
13
|
-
import { Route, Router } from 'react-router';
|
14
|
-
|
15
|
-
import { InstagramAPI } from '../__fixtures__/api-descriptions/Instagram';
|
16
|
-
import { simpleApiWithoutDescription } from '../__fixtures__/api-descriptions/simpleApiWithoutDescription';
|
17
|
-
import { API, APIImpl } from './API';
|
18
|
-
|
19
|
-
export const APIWithoutRouter = flow(
|
20
|
-
withStyles,
|
21
|
-
withPersistenceBoundary,
|
22
|
-
withMosaicProvider,
|
23
|
-
withQueryClientProvider,
|
24
|
-
)(APIImpl);
|
25
|
-
|
26
|
-
describe('API', () => {
|
27
|
-
const APIDocument = {
|
28
|
-
...InstagramAPI,
|
29
|
-
info: {
|
30
|
-
...InstagramAPI.info,
|
31
|
-
'x-logo': {
|
32
|
-
...InstagramAPI.info['x-logo'],
|
33
|
-
altText: 'instagram-logo',
|
34
|
-
},
|
35
|
-
},
|
36
|
-
};
|
37
|
-
|
38
|
-
// we need to add scrollTo to the Element prototype before we mount so it has the method available
|
39
|
-
Element.prototype.scrollTo = () => {};
|
40
|
-
|
41
|
-
it('displays logo specified in x-logo property of API document', async () => {
|
42
|
-
render(<API layout="sidebar" apiDescriptionDocument={InstagramAPI} />);
|
43
|
-
|
44
|
-
// checks if altText defaults to "logo" if the prop is not passed in API document
|
45
|
-
// checks if logo is present
|
46
|
-
expect(await screen.findByAltText('logo')).toBeInTheDocument();
|
47
|
-
});
|
48
|
-
|
49
|
-
it('uses the altText property from the API document', async () => {
|
50
|
-
render(<API layout="sidebar" apiDescriptionDocument={APIDocument} />);
|
51
|
-
|
52
|
-
expect(await screen.findByAltText('instagram-logo')).toBeInTheDocument();
|
53
|
-
});
|
54
|
-
|
55
|
-
it("doesn't display the logo when no properties are passed neither via API document nor as component prop", () => {
|
56
|
-
render(<API layout="sidebar" apiDescriptionDocument={simpleApiWithoutDescription} />);
|
57
|
-
|
58
|
-
expect(screen.queryByAltText('logo')).not.toBeInTheDocument();
|
59
|
-
});
|
60
|
-
|
61
|
-
it('overrides the logo from API document with the one passed in a prop', async () => {
|
62
|
-
render(<API logo="thisisarequiredprop" layout="sidebar" apiDescriptionDocument={APIDocument} />);
|
63
|
-
|
64
|
-
expect(screen.queryByAltText('instagram-logo')).not.toBeInTheDocument();
|
65
|
-
expect(await screen.findByAltText('logo')).toBeInTheDocument();
|
66
|
-
});
|
67
|
-
|
68
|
-
it('displays internal operations by default', () => {
|
69
|
-
const history = createMemoryHistory();
|
70
|
-
history.push('/paths/internal-operation/get');
|
71
|
-
|
72
|
-
const { unmount } = render(
|
73
|
-
<Router history={history}>
|
74
|
-
<Route path="/">
|
75
|
-
<APIWithoutRouter layout="sidebar" apiDescriptionDocument={APIDocument} />
|
76
|
-
</Route>
|
77
|
-
</Router>,
|
78
|
-
);
|
79
|
-
|
80
|
-
expect(screen.getByText('If you see this, something went wrong')).toBeInTheDocument();
|
81
|
-
|
82
|
-
unmount();
|
83
|
-
});
|
84
|
-
|
85
|
-
it('displays internal models by default', () => {
|
86
|
-
const history = createMemoryHistory();
|
87
|
-
history.push('/schemas/InternalObject');
|
88
|
-
|
89
|
-
render(
|
90
|
-
<Router history={history}>
|
91
|
-
<Route path="/">
|
92
|
-
<APIWithoutRouter layout="sidebar" apiDescriptionDocument={APIDocument} />
|
93
|
-
</Route>
|
94
|
-
</Router>,
|
95
|
-
);
|
96
|
-
|
97
|
-
expect(screen.getByText('Cool object, but internal.')).toBeInTheDocument();
|
98
|
-
});
|
99
|
-
|
100
|
-
it('reroutes to main page on internal operation if hideInternal is on', () => {
|
101
|
-
const history = createMemoryHistory();
|
102
|
-
history.push('/paths/internal-operation/get');
|
103
|
-
|
104
|
-
render(
|
105
|
-
<Router history={history}>
|
106
|
-
<Route path="/">
|
107
|
-
<APIWithoutRouter layout="sidebar" apiDescriptionDocument={APIDocument} hideInternal />
|
108
|
-
</Route>
|
109
|
-
</Router>,
|
110
|
-
);
|
111
|
-
|
112
|
-
expect(screen.queryByText('If you see this, something went wrong')).not.toBeInTheDocument();
|
113
|
-
expect(history.location.pathname).toBe('/');
|
114
|
-
});
|
115
|
-
|
116
|
-
it('reroutes to main page on internal model if hideInternal is on', () => {
|
117
|
-
const history = createMemoryHistory();
|
118
|
-
history.push('/schemas/InternalObject');
|
119
|
-
|
120
|
-
render(
|
121
|
-
<Router history={history}>
|
122
|
-
<Route path="/">
|
123
|
-
<APIWithoutRouter layout="sidebar" apiDescriptionDocument={APIDocument} hideInternal />
|
124
|
-
</Route>
|
125
|
-
</Router>,
|
126
|
-
);
|
127
|
-
|
128
|
-
expect(screen.queryByText('Cool object, but internal.')).not.toBeInTheDocument();
|
129
|
-
expect(history.location.pathname).toBe('/');
|
130
|
-
});
|
131
|
-
});
|
@@ -1,99 +0,0 @@
|
|
1
|
-
import { parse } from '@stoplight/yaml';
|
2
|
-
import { Story } from '@storybook/react';
|
3
|
-
import * as React from 'react';
|
4
|
-
|
5
|
-
import { badgesForSchema } from '../__fixtures__/api-descriptions/badgesForSchema';
|
6
|
-
import { simpleApiWithInternalOperations } from '../__fixtures__/api-descriptions/simpleApiWithInternalOperations';
|
7
|
-
import { simpleApiWithoutDescription } from '../__fixtures__/api-descriptions/simpleApiWithoutDescription';
|
8
|
-
import { todosApiBundled } from '../__fixtures__/api-descriptions/todosApiBundled';
|
9
|
-
import { zoomApiYaml } from '../__fixtures__/api-descriptions/zoomApiYaml';
|
10
|
-
import { API, APIProps } from './API';
|
11
|
-
|
12
|
-
export default {
|
13
|
-
title: 'Public/API',
|
14
|
-
component: API,
|
15
|
-
argTypes: {
|
16
|
-
apiDescriptionDocument: { control: 'text', type: { required: false }, table: { category: 'Input' } },
|
17
|
-
apiDescriptionUrl: { control: 'text', table: { category: 'Input' } },
|
18
|
-
layout: {
|
19
|
-
control: { type: 'inline-radio' },
|
20
|
-
table: { category: 'UI' },
|
21
|
-
},
|
22
|
-
basePath: { table: { category: 'Routing' } },
|
23
|
-
router: { table: { category: 'Routing' } },
|
24
|
-
},
|
25
|
-
args: {
|
26
|
-
router: 'memory',
|
27
|
-
},
|
28
|
-
};
|
29
|
-
|
30
|
-
const Template: Story<APIProps> = args => <API {...args} />;
|
31
|
-
|
32
|
-
export const APIWithYamlProvidedDirectly = Template.bind({});
|
33
|
-
APIWithYamlProvidedDirectly.args = {
|
34
|
-
apiDescriptionDocument: zoomApiYaml,
|
35
|
-
};
|
36
|
-
APIWithYamlProvidedDirectly.storyName = 'Direct YAML Input (Zoom)';
|
37
|
-
|
38
|
-
export const APIWithJSONProvidedDirectly = Template.bind({});
|
39
|
-
APIWithJSONProvidedDirectly.args = {
|
40
|
-
apiDescriptionDocument: JSON.stringify(parse(zoomApiYaml), null, ' '),
|
41
|
-
};
|
42
|
-
APIWithJSONProvidedDirectly.storyName = 'Direct JSON Input (Zoom)';
|
43
|
-
|
44
|
-
export const APIWithoutDescription = Template.bind({});
|
45
|
-
APIWithoutDescription.args = {
|
46
|
-
apiDescriptionDocument: JSON.stringify(simpleApiWithoutDescription, null, 2),
|
47
|
-
};
|
48
|
-
APIWithoutDescription.storyName = 'API Without Description';
|
49
|
-
|
50
|
-
export const APIWithInternalOperations = Template.bind({});
|
51
|
-
APIWithInternalOperations.args = {
|
52
|
-
apiDescriptionDocument: JSON.stringify(simpleApiWithInternalOperations, null, 2),
|
53
|
-
};
|
54
|
-
APIWithInternalOperations.storyName = 'API With Internal Operations';
|
55
|
-
|
56
|
-
export const OpenApi3Schema = Template.bind({});
|
57
|
-
OpenApi3Schema.args = {
|
58
|
-
apiDescriptionDocument: todosApiBundled,
|
59
|
-
};
|
60
|
-
OpenApi3Schema.storyName = 'Open Api 3.0 Schema';
|
61
|
-
|
62
|
-
export const BadgesForSchema = Template.bind({});
|
63
|
-
BadgesForSchema.args = {
|
64
|
-
apiDescriptionDocument: badgesForSchema,
|
65
|
-
};
|
66
|
-
BadgesForSchema.storyName = 'Badges For Schema';
|
67
|
-
|
68
|
-
export const StackedLayout = Template.bind({});
|
69
|
-
StackedLayout.args = {
|
70
|
-
apiDescriptionDocument: JSON.stringify(parse(zoomApiYaml), null, ' '),
|
71
|
-
layout: 'stacked',
|
72
|
-
};
|
73
|
-
StackedLayout.storyName = 'Stacked Layout (Zoom)';
|
74
|
-
|
75
|
-
export const Box = Template.bind({});
|
76
|
-
Box.args = {
|
77
|
-
apiDescriptionUrl: 'https://raw.githubusercontent.com/box/box-openapi/main/content/openapi.yml',
|
78
|
-
};
|
79
|
-
Box.storyName = 'Box';
|
80
|
-
|
81
|
-
export const DigitalOcean = Template.bind({});
|
82
|
-
DigitalOcean.args = {
|
83
|
-
apiDescriptionUrl:
|
84
|
-
'https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml',
|
85
|
-
};
|
86
|
-
DigitalOcean.storyName = 'DigitalOcean';
|
87
|
-
|
88
|
-
export const Github = Template.bind({});
|
89
|
-
Github.args = {
|
90
|
-
apiDescriptionUrl:
|
91
|
-
'https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/ghes-3.0/ghes-3.0.json',
|
92
|
-
};
|
93
|
-
Github.storyName = 'GitHub';
|
94
|
-
|
95
|
-
export const Instagram = Template.bind({});
|
96
|
-
Instagram.args = {
|
97
|
-
apiDescriptionUrl: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml',
|
98
|
-
};
|
99
|
-
Instagram.storyName = 'Instagram';
|
package/src/containers/API.tsx
DELETED
@@ -1,200 +0,0 @@
|
|
1
|
-
import {
|
2
|
-
InlineRefResolverProvider,
|
3
|
-
NonIdealState,
|
4
|
-
RoutingProps,
|
5
|
-
useBundleRefsIntoDocument,
|
6
|
-
useParsedValue,
|
7
|
-
withMosaicProvider,
|
8
|
-
withPersistenceBoundary,
|
9
|
-
withQueryClientProvider,
|
10
|
-
withRouter,
|
11
|
-
withStyles,
|
12
|
-
} from '@stoplight/elements-core';
|
13
|
-
import { Box, Flex, Icon } from '@stoplight/mosaic';
|
14
|
-
import { flow } from 'lodash';
|
15
|
-
import * as React from 'react';
|
16
|
-
import { useQuery } from 'react-query';
|
17
|
-
|
18
|
-
import { APIWithSidebarLayout } from '../components/API/APIWithSidebarLayout';
|
19
|
-
import { APIWithStackedLayout } from '../components/API/APIWithStackedLayout';
|
20
|
-
import { useExportDocumentProps } from '../hooks/useExportDocumentProps';
|
21
|
-
import { transformOasToServiceNode } from '../utils/oas';
|
22
|
-
|
23
|
-
export type APIProps = APIPropsWithDocument | APIPropsWithUrl;
|
24
|
-
|
25
|
-
export type APIPropsWithUrl = {
|
26
|
-
/**
|
27
|
-
* Specify the URL of the input OAS2/3 document here.
|
28
|
-
*
|
29
|
-
* Mutually exclusive with `apiDescriptionDocument`.
|
30
|
-
*/
|
31
|
-
apiDescriptionUrl: string;
|
32
|
-
} & CommonAPIProps;
|
33
|
-
export type APIPropsWithDocument = {
|
34
|
-
/**
|
35
|
-
* You can specify the input OAS2/3 document here directly in JSON or YAML format.
|
36
|
-
*
|
37
|
-
* Mutually exclusive with `apiDescriptionUrl`.
|
38
|
-
*/
|
39
|
-
apiDescriptionDocument: string | object;
|
40
|
-
apiDescriptionUrl?: string;
|
41
|
-
} & CommonAPIProps;
|
42
|
-
|
43
|
-
export interface CommonAPIProps extends RoutingProps {
|
44
|
-
/**
|
45
|
-
* The API component supports two layout options.
|
46
|
-
*
|
47
|
-
* - Sidebar: Navigation on the left side, resembles Stoplight Platform.
|
48
|
-
* - Stacked: No sidebar, resembles the structure of Swagger UI.
|
49
|
-
*
|
50
|
-
* @default "sidebar"
|
51
|
-
*/
|
52
|
-
layout?: 'sidebar' | 'stacked';
|
53
|
-
logo?: string;
|
54
|
-
|
55
|
-
/**
|
56
|
-
* Allows hiding the TryIt component
|
57
|
-
*/
|
58
|
-
hideTryIt?: boolean;
|
59
|
-
|
60
|
-
/**
|
61
|
-
* Hides schemas from being displayed in Table of Contents
|
62
|
-
*/
|
63
|
-
hideSchemas?: boolean;
|
64
|
-
|
65
|
-
/**
|
66
|
-
* Hides models and operations marked as internal
|
67
|
-
* @default false
|
68
|
-
*/
|
69
|
-
hideInternal?: boolean;
|
70
|
-
|
71
|
-
/**
|
72
|
-
* Hides export button from being displayed in overview page
|
73
|
-
* @default false
|
74
|
-
*/
|
75
|
-
hideExport?: boolean;
|
76
|
-
|
77
|
-
/**
|
78
|
-
* Fetch credentials policy for TryIt component
|
79
|
-
* For more information: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
|
80
|
-
* @default "omit"
|
81
|
-
*/
|
82
|
-
|
83
|
-
tryItCredentialsPolicy?: 'omit' | 'include' | 'same-origin';
|
84
|
-
|
85
|
-
/**
|
86
|
-
* Url of a CORS proxy that will be used to send requests in TryIt.
|
87
|
-
* Provided url will be prepended to an URL of an actual request.
|
88
|
-
* @default false
|
89
|
-
*/
|
90
|
-
tryItCorsProxy?: string;
|
91
|
-
tryItOutDefaultServer?: string;
|
92
|
-
}
|
93
|
-
|
94
|
-
const propsAreWithDocument = (props: APIProps): props is APIPropsWithDocument => {
|
95
|
-
return props.hasOwnProperty('apiDescriptionDocument');
|
96
|
-
};
|
97
|
-
|
98
|
-
export const APIImpl: React.FC<APIProps> = props => {
|
99
|
-
const {
|
100
|
-
layout,
|
101
|
-
apiDescriptionUrl = '',
|
102
|
-
logo,
|
103
|
-
hideTryIt,
|
104
|
-
hideSchemas,
|
105
|
-
hideInternal,
|
106
|
-
hideExport,
|
107
|
-
tryItCredentialsPolicy,
|
108
|
-
tryItCorsProxy,
|
109
|
-
tryItOutDefaultServer,
|
110
|
-
} = props;
|
111
|
-
const apiDescriptionDocument = propsAreWithDocument(props) ? props.apiDescriptionDocument : undefined;
|
112
|
-
|
113
|
-
const { data: fetchedDocument, error } = useQuery(
|
114
|
-
[apiDescriptionUrl],
|
115
|
-
() =>
|
116
|
-
fetch(apiDescriptionUrl).then(res => {
|
117
|
-
if (res.ok) {
|
118
|
-
return res.text();
|
119
|
-
}
|
120
|
-
throw new Error(`Unable to load description document, status code: ${res.status}`);
|
121
|
-
}),
|
122
|
-
{
|
123
|
-
enabled: apiDescriptionUrl !== '' && !apiDescriptionDocument,
|
124
|
-
},
|
125
|
-
);
|
126
|
-
|
127
|
-
const document = apiDescriptionDocument || fetchedDocument || '';
|
128
|
-
const parsedDocument = useParsedValue(document);
|
129
|
-
const bundledDocument = useBundleRefsIntoDocument(parsedDocument, { baseUrl: apiDescriptionUrl });
|
130
|
-
const serviceNode = React.useMemo(() => transformOasToServiceNode(bundledDocument), [bundledDocument]);
|
131
|
-
const exportProps = useExportDocumentProps({ originalDocument: document, bundledDocument });
|
132
|
-
|
133
|
-
if (error) {
|
134
|
-
return (
|
135
|
-
<Flex justify="center" alignItems="center" w="full" minH="screen">
|
136
|
-
<NonIdealState
|
137
|
-
title="Document could not be loaded"
|
138
|
-
description="The API description document could not be fetched. This could indicate connectivity problems, or issues with the server hosting the spec."
|
139
|
-
icon="exclamation-triangle"
|
140
|
-
/>
|
141
|
-
</Flex>
|
142
|
-
);
|
143
|
-
}
|
144
|
-
|
145
|
-
if (!bundledDocument) {
|
146
|
-
return (
|
147
|
-
<Flex justify="center" alignItems="center" w="full" minH="screen" color="light">
|
148
|
-
<Box as={Icon} icon={['fal', 'circle-notch']} size="3x" spin />
|
149
|
-
</Flex>
|
150
|
-
);
|
151
|
-
}
|
152
|
-
|
153
|
-
if (!serviceNode) {
|
154
|
-
return (
|
155
|
-
<Flex justify="center" alignItems="center" w="full" minH="screen">
|
156
|
-
<NonIdealState
|
157
|
-
title="Failed to parse OpenAPI file"
|
158
|
-
description="Please make sure your OpenAPI file is valid and try again"
|
159
|
-
/>
|
160
|
-
</Flex>
|
161
|
-
);
|
162
|
-
}
|
163
|
-
|
164
|
-
return (
|
165
|
-
<InlineRefResolverProvider document={parsedDocument}>
|
166
|
-
{layout === 'stacked' ? (
|
167
|
-
<APIWithStackedLayout
|
168
|
-
serviceNode={serviceNode}
|
169
|
-
hideTryIt={hideTryIt}
|
170
|
-
hideExport={hideExport}
|
171
|
-
exportProps={exportProps}
|
172
|
-
tryItCredentialsPolicy={tryItCredentialsPolicy}
|
173
|
-
tryItCorsProxy={tryItCorsProxy}
|
174
|
-
tryItOutDefaultServer={tryItOutDefaultServer}
|
175
|
-
/>
|
176
|
-
) : (
|
177
|
-
<APIWithSidebarLayout
|
178
|
-
logo={logo}
|
179
|
-
serviceNode={serviceNode}
|
180
|
-
hideTryIt={hideTryIt}
|
181
|
-
hideSchemas={hideSchemas}
|
182
|
-
hideInternal={hideInternal}
|
183
|
-
hideExport={hideExport}
|
184
|
-
exportProps={exportProps}
|
185
|
-
tryItCredentialsPolicy={tryItCredentialsPolicy}
|
186
|
-
tryItCorsProxy={tryItCorsProxy}
|
187
|
-
tryItOutDefaultServer={tryItOutDefaultServer}
|
188
|
-
/>
|
189
|
-
)}
|
190
|
-
</InlineRefResolverProvider>
|
191
|
-
);
|
192
|
-
};
|
193
|
-
|
194
|
-
export const API = flow(
|
195
|
-
withRouter,
|
196
|
-
withStyles,
|
197
|
-
withPersistenceBoundary,
|
198
|
-
withMosaicProvider,
|
199
|
-
withQueryClientProvider,
|
200
|
-
)(APIImpl);
|
@@ -1,68 +0,0 @@
|
|
1
|
-
import '@testing-library/jest-dom';
|
2
|
-
|
3
|
-
import { safeStringify } from '@stoplight/yaml';
|
4
|
-
import { act, renderHook } from '@testing-library/react-hooks';
|
5
|
-
import { saveAs } from 'file-saver';
|
6
|
-
|
7
|
-
import { InstagramAPI as bundledJson } from '../__fixtures__/api-descriptions/Instagram';
|
8
|
-
import { simpleApiWithoutDescription as json } from '../__fixtures__/api-descriptions/simpleApiWithoutDescription';
|
9
|
-
import { todosApiBundled as bundledYaml } from '../__fixtures__/api-descriptions/todosApiBundled';
|
10
|
-
import { zoomApiYaml as yaml } from '../__fixtures__/api-descriptions/zoomApiYaml';
|
11
|
-
import { useExportDocumentProps } from './useExportDocumentProps';
|
12
|
-
|
13
|
-
jest.mock('file-saver');
|
14
|
-
|
15
|
-
describe('useExportDocumentProps', () => {
|
16
|
-
afterEach(() => {
|
17
|
-
jest.resetAllMocks();
|
18
|
-
});
|
19
|
-
it('exports json document', () => {
|
20
|
-
const data = renderHook(() =>
|
21
|
-
useExportDocumentProps({
|
22
|
-
originalDocument: json,
|
23
|
-
bundledDocument: bundledJson,
|
24
|
-
}),
|
25
|
-
);
|
26
|
-
|
27
|
-
act(() => {
|
28
|
-
data.result.current.original.onPress();
|
29
|
-
data.result.current.bundled.onPress();
|
30
|
-
});
|
31
|
-
|
32
|
-
const expectedOriginalDocument = new Blob([JSON.stringify(json, null, 2)], {
|
33
|
-
type: 'application/json',
|
34
|
-
});
|
35
|
-
|
36
|
-
const expectedBundledDocument = new Blob([JSON.stringify(bundledJson, null, 2)], {
|
37
|
-
type: 'application/json',
|
38
|
-
});
|
39
|
-
expect(saveAs).toBeCalledTimes(2);
|
40
|
-
expect(saveAs).toHaveBeenCalledWith(expectedOriginalDocument, 'document.json');
|
41
|
-
expect(saveAs).toHaveBeenCalledWith(expectedBundledDocument, 'document.json');
|
42
|
-
});
|
43
|
-
|
44
|
-
it('exports yaml document', () => {
|
45
|
-
const data = renderHook(() =>
|
46
|
-
useExportDocumentProps({
|
47
|
-
originalDocument: safeStringify(yaml),
|
48
|
-
bundledDocument: bundledYaml,
|
49
|
-
}),
|
50
|
-
);
|
51
|
-
|
52
|
-
act(() => {
|
53
|
-
data.result.current.original.onPress();
|
54
|
-
data.result.current.bundled.onPress();
|
55
|
-
});
|
56
|
-
|
57
|
-
const expectedOriginalDocument = new Blob([safeStringify(yaml)], {
|
58
|
-
type: 'application/yaml',
|
59
|
-
});
|
60
|
-
|
61
|
-
const expectedBundledDocument = new Blob([safeStringify(bundledYaml)], {
|
62
|
-
type: 'application/yaml',
|
63
|
-
});
|
64
|
-
expect(saveAs).toBeCalledTimes(2);
|
65
|
-
expect(saveAs).toHaveBeenCalledWith(expectedOriginalDocument, 'document.yaml');
|
66
|
-
expect(saveAs).toHaveBeenCalledWith(expectedBundledDocument, 'document.yaml');
|
67
|
-
});
|
68
|
-
});
|