@jpmorganchase/elemental 1.0.0

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.
Files changed (35) hide show
  1. package/.storybook/main.js +1 -0
  2. package/.storybook/manager.js +1 -0
  3. package/.storybook/preview.jsx +3 -0
  4. package/LICENSE +190 -0
  5. package/README.md +19 -0
  6. package/jest.config.js +7 -0
  7. package/package.json +111 -0
  8. package/src/__fixtures__/api-descriptions/Instagram.ts +1859 -0
  9. package/src/__fixtures__/api-descriptions/badgesForSchema.ts +36 -0
  10. package/src/__fixtures__/api-descriptions/simpleApiWithInternalOperations.ts +253 -0
  11. package/src/__fixtures__/api-descriptions/simpleApiWithoutDescription.ts +243 -0
  12. package/src/__fixtures__/api-descriptions/todosApiBundled.ts +430 -0
  13. package/src/__fixtures__/api-descriptions/zoomApiYaml.ts +6083 -0
  14. package/src/components/API/APIWithSidebarLayout.tsx +111 -0
  15. package/src/components/API/APIWithStackedLayout.tsx +220 -0
  16. package/src/components/API/__tests__/utils.test.ts +848 -0
  17. package/src/components/API/utils.ts +174 -0
  18. package/src/containers/API.spec.tsx +131 -0
  19. package/src/containers/API.stories.tsx +99 -0
  20. package/src/containers/API.tsx +200 -0
  21. package/src/hooks/useExportDocumentProps.spec.tsx +68 -0
  22. package/src/hooks/useExportDocumentProps.tsx +48 -0
  23. package/src/index.ts +2 -0
  24. package/src/styles.css +1 -0
  25. package/src/utils/oas/__tests__/oas.spec.ts +272 -0
  26. package/src/utils/oas/index.ts +150 -0
  27. package/src/utils/oas/oas2.ts +31 -0
  28. package/src/utils/oas/oas3.ts +37 -0
  29. package/src/utils/oas/types.ts +31 -0
  30. package/src/web-components/__stories__/Api.stories.tsx +63 -0
  31. package/src/web-components/components.ts +20 -0
  32. package/src/web-components/index.ts +3 -0
  33. package/tsconfig.build.json +18 -0
  34. package/tsconfig.json +7 -0
  35. package/web-components.config.js +1 -0
@@ -0,0 +1,111 @@
1
+ import {
2
+ ExportButtonProps,
3
+ Logo,
4
+ ParsedDocs,
5
+ PoweredByLink,
6
+ SidebarLayout,
7
+ TableOfContents,
8
+ } from '@stoplight/elements-core';
9
+ import { Flex, Heading } from '@stoplight/mosaic';
10
+ import { NodeType } from '@stoplight/types';
11
+ import * as React from 'react';
12
+ import { Link, Redirect, useLocation } from 'react-router-dom';
13
+
14
+ import { ServiceNode } from '../../utils/oas/types';
15
+ import { computeAPITree, findFirstNodeSlug, isInternal } from './utils';
16
+
17
+ type SidebarLayoutProps = {
18
+ serviceNode: ServiceNode;
19
+ logo?: string;
20
+ hideTryIt?: boolean;
21
+ hideSchemas?: boolean;
22
+ hideInternal?: boolean;
23
+ hideExport?: boolean;
24
+ exportProps?: ExportButtonProps;
25
+ tryItCredentialsPolicy?: 'omit' | 'include' | 'same-origin';
26
+ tryItCorsProxy?: string;
27
+ tryItOutDefaultServer?: string;
28
+ };
29
+
30
+ export const APIWithSidebarLayout: React.FC<SidebarLayoutProps> = ({
31
+ serviceNode,
32
+ logo,
33
+ hideTryIt,
34
+ hideSchemas,
35
+ hideInternal,
36
+ hideExport,
37
+ exportProps,
38
+ tryItCredentialsPolicy,
39
+ tryItCorsProxy,
40
+ tryItOutDefaultServer,
41
+ }) => {
42
+ const container = React.useRef<HTMLDivElement>(null);
43
+ const tree = React.useMemo(
44
+ () => computeAPITree(serviceNode, { hideSchemas, hideInternal }),
45
+ [serviceNode, hideSchemas, hideInternal],
46
+ );
47
+ const location = useLocation();
48
+ const { pathname } = location;
49
+ const isRootPath = !pathname || pathname === '/';
50
+ const node = isRootPath ? serviceNode : serviceNode.children.find(child => child.uri === pathname);
51
+
52
+ const layoutOptions = React.useMemo(
53
+ () => ({ hideTryIt: hideTryIt, hideExport: hideExport || node?.type !== NodeType.HttpService }),
54
+ [hideTryIt, hideExport, node],
55
+ );
56
+
57
+ if (!node) {
58
+ // Redirect to the first child if node doesn't exist
59
+ const firstSlug = findFirstNodeSlug(tree);
60
+
61
+ if (firstSlug) {
62
+ return <Redirect to={firstSlug} />;
63
+ }
64
+ }
65
+
66
+ if (hideInternal && node && isInternal(node)) {
67
+ return <Redirect to="/" />;
68
+ }
69
+
70
+ const handleTocClick = () => {
71
+ if (container.current) {
72
+ container.current.scrollIntoView();
73
+ }
74
+ };
75
+
76
+ const sidebar = (
77
+ <>
78
+ <Flex ml={4} mb={5} alignItems="center">
79
+ {logo ? (
80
+ <Logo logo={{ url: logo, altText: 'logo' }} />
81
+ ) : (
82
+ serviceNode.data.logo && <Logo logo={serviceNode.data.logo} />
83
+ )}
84
+ <Heading size={4}>{serviceNode.name}</Heading>
85
+ </Flex>
86
+ <Flex flexGrow flexShrink overflowY="auto" direction="col">
87
+ <TableOfContents tree={tree} activeId={pathname} Link={Link} onLinkClick={handleTocClick} />
88
+ </Flex>
89
+ <PoweredByLink source={serviceNode.name} pathname={pathname} packageType="elements" />
90
+ </>
91
+ );
92
+
93
+ return (
94
+ <SidebarLayout ref={container} sidebar={sidebar}>
95
+ {node && (
96
+ <ParsedDocs
97
+ key={pathname}
98
+ uri={pathname}
99
+ node={node}
100
+ nodeTitle={node.name}
101
+ layoutOptions={layoutOptions}
102
+ location={location}
103
+ exportProps={exportProps}
104
+ tryItCredentialsPolicy={tryItCredentialsPolicy}
105
+ tryItCorsProxy={tryItCorsProxy}
106
+ tryItOutDefaultServer={tryItOutDefaultServer}
107
+ />
108
+ )}
109
+ </SidebarLayout>
110
+ );
111
+ };
@@ -0,0 +1,220 @@
1
+ import {
2
+ DeprecatedBadge,
3
+ Docs,
4
+ ExportButtonProps,
5
+ HttpMethodColors,
6
+ ParsedDocs,
7
+ TryItWithRequestSamples,
8
+ } from '@stoplight/elements-core';
9
+ import { Box, Flex, Icon, Tab, TabList, TabPanel, TabPanels, Tabs } from '@stoplight/mosaic';
10
+ import { NodeType } from '@stoplight/types';
11
+ import cn from 'classnames';
12
+ import * as React from 'react';
13
+ import { useLocation } from 'react-router-dom';
14
+
15
+ import { OperationNode, ServiceNode } from '../../utils/oas/types';
16
+ import { computeTagGroups, TagGroup } from './utils';
17
+
18
+ type TryItCredentialsPolicy = 'omit' | 'include' | 'same-origin';
19
+
20
+ type StackedLayoutProps = {
21
+ serviceNode: ServiceNode;
22
+ hideTryIt?: boolean;
23
+ hideExport?: boolean;
24
+ exportProps?: ExportButtonProps;
25
+ tryItCredentialsPolicy?: TryItCredentialsPolicy;
26
+ tryItCorsProxy?: string;
27
+ tryItOutDefaultServer?: string;
28
+ };
29
+
30
+ const itemMatchesHash = (hash: string, item: OperationNode) => {
31
+ return hash.substr(1) === `${item.name}-${item.data.method}`;
32
+ };
33
+
34
+ const TryItContext = React.createContext<{
35
+ hideTryIt?: boolean;
36
+ tryItCredentialsPolicy?: TryItCredentialsPolicy;
37
+ corsProxy?: string;
38
+ }>({
39
+ hideTryIt: false,
40
+ tryItCredentialsPolicy: 'omit',
41
+ });
42
+ TryItContext.displayName = 'TryItContext';
43
+
44
+ export const APIWithStackedLayout: React.FC<StackedLayoutProps> = ({
45
+ serviceNode,
46
+ hideTryIt,
47
+ hideExport,
48
+ exportProps,
49
+ tryItCredentialsPolicy,
50
+ tryItCorsProxy,
51
+ tryItOutDefaultServer,
52
+ }) => {
53
+ const location = useLocation();
54
+ const { groups } = computeTagGroups(serviceNode);
55
+
56
+ return (
57
+ <TryItContext.Provider value={{ hideTryIt, tryItCredentialsPolicy, corsProxy: tryItCorsProxy }}>
58
+ <Flex w="full" flexDirection="col" m="auto" className="sl-max-w-4xl">
59
+ <Box w="full" borderB>
60
+ <Docs
61
+ className="sl-mx-auto"
62
+ nodeData={serviceNode.data}
63
+ nodeTitle={serviceNode.name}
64
+ nodeType={NodeType.HttpService}
65
+ location={location}
66
+ layoutOptions={{ showPoweredByLink: true, hideExport }}
67
+ exportProps={exportProps}
68
+ tryItCredentialsPolicy={tryItCredentialsPolicy}
69
+ tryItOutDefaultServer={tryItOutDefaultServer}
70
+ />
71
+ </Box>
72
+
73
+ {groups.map(group => (
74
+ <Group key={group.title} group={group} />
75
+ ))}
76
+ </Flex>
77
+ </TryItContext.Provider>
78
+ );
79
+ };
80
+
81
+ const Group = React.memo<{ group: TagGroup }>(({ group }) => {
82
+ const [isExpanded, setIsExpanded] = React.useState(false);
83
+ const { hash } = useLocation();
84
+ const scrollRef = React.useRef<HTMLDivElement | null>(null);
85
+ const urlHashMatches = hash.substr(1) === group.title;
86
+
87
+ const onClick = React.useCallback(() => setIsExpanded(!isExpanded), [isExpanded]);
88
+
89
+ const shouldExpand = React.useMemo(() => {
90
+ return urlHashMatches || group.items.some(item => itemMatchesHash(hash, item));
91
+ }, [group, hash, urlHashMatches]);
92
+
93
+ React.useEffect(() => {
94
+ if (shouldExpand) {
95
+ setIsExpanded(true);
96
+ if (urlHashMatches && scrollRef?.current?.offsetTop) {
97
+ // scroll only if group is active
98
+ window.scrollTo(0, scrollRef.current.offsetTop);
99
+ }
100
+ }
101
+ }, [shouldExpand, urlHashMatches, group, hash]);
102
+
103
+ return (
104
+ <Box>
105
+ <Flex
106
+ ref={scrollRef}
107
+ onClick={onClick}
108
+ mx="auto"
109
+ justifyContent="between"
110
+ alignItems="center"
111
+ borderB
112
+ px={2}
113
+ py={4}
114
+ cursor="pointer"
115
+ color={{ default: 'current', hover: 'muted' }}
116
+ >
117
+ <Box fontSize="lg" fontWeight="medium">
118
+ {group.title}
119
+ </Box>
120
+ <Icon className="sl-mr-2" icon={isExpanded ? 'chevron-down' : 'chevron-right'} size="sm" />
121
+ </Flex>
122
+
123
+ <Collapse isOpen={isExpanded}>
124
+ {group.items.map(item => {
125
+ return <Item key={item.uri} item={item} />;
126
+ })}
127
+ </Collapse>
128
+ </Box>
129
+ );
130
+ });
131
+
132
+ const Item = React.memo<{ item: OperationNode }>(({ item }) => {
133
+ const location = useLocation();
134
+ const { hash } = location;
135
+ const [isExpanded, setIsExpanded] = React.useState(false);
136
+ const scrollRef = React.useRef<HTMLDivElement | null>(null);
137
+ const color = HttpMethodColors[item.data.method] || 'gray';
138
+ const isDeprecated = !!item.data.deprecated;
139
+ const { hideTryIt, tryItCredentialsPolicy, corsProxy } = React.useContext(TryItContext);
140
+
141
+ const onClick = React.useCallback(() => setIsExpanded(!isExpanded), [isExpanded]);
142
+
143
+ React.useEffect(() => {
144
+ if (itemMatchesHash(hash, item)) {
145
+ setIsExpanded(true);
146
+ if (scrollRef?.current?.offsetTop) {
147
+ window.scrollTo(0, scrollRef.current.offsetTop);
148
+ }
149
+ }
150
+ }, [hash, item]);
151
+
152
+ return (
153
+ <Box
154
+ ref={scrollRef}
155
+ w="full"
156
+ my={2}
157
+ border
158
+ borderColor={{ default: isExpanded ? 'light' : 'transparent', hover: 'light' }}
159
+ bg={{ default: isExpanded ? 'code' : 'transparent', hover: 'code' }}
160
+ >
161
+ <Flex mx="auto" alignItems="center" cursor="pointer" fontSize="lg" p={2} onClick={onClick} color="current">
162
+ <Box
163
+ w={24}
164
+ textTransform="uppercase"
165
+ textAlign="center"
166
+ fontWeight="semibold"
167
+ border
168
+ rounded
169
+ px={2}
170
+ bg="canvas"
171
+ className={cn(`sl-mr-5 sl-text-base`, `sl-text-${color}`, `sl-border-${color}`)}
172
+ >
173
+ {item.data.method || 'UNKNOWN'}
174
+ </Box>
175
+
176
+ <Box flex={1} fontWeight="medium" wordBreak="all">
177
+ {item.name}
178
+ </Box>
179
+ {isDeprecated && <DeprecatedBadge />}
180
+ </Flex>
181
+
182
+ <Collapse isOpen={isExpanded}>
183
+ {hideTryIt ? (
184
+ <Box as={ParsedDocs} layoutOptions={{ noHeading: true, hideTryItPanel: true }} node={item} p={4} />
185
+ ) : (
186
+ <Tabs appearance="line">
187
+ <TabList>
188
+ <Tab>Docs</Tab>
189
+ <Tab>TryIt</Tab>
190
+ </TabList>
191
+
192
+ <TabPanels>
193
+ <TabPanel>
194
+ <ParsedDocs
195
+ className="sl-px-4"
196
+ node={item}
197
+ location={location}
198
+ layoutOptions={{ noHeading: true, hideTryItPanel: true }}
199
+ />
200
+ </TabPanel>
201
+ <TabPanel>
202
+ <TryItWithRequestSamples
203
+ httpOperation={item.data}
204
+ tryItCredentialsPolicy={tryItCredentialsPolicy}
205
+ corsProxy={corsProxy}
206
+ />
207
+ </TabPanel>
208
+ </TabPanels>
209
+ </Tabs>
210
+ )}
211
+ </Collapse>
212
+ </Box>
213
+ );
214
+ });
215
+
216
+ const Collapse: React.FC<{ isOpen: boolean }> = ({ isOpen, children }) => {
217
+ if (!isOpen) return null;
218
+
219
+ return <Box>{children}</Box>;
220
+ };