@jpmorganchase/elemental 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ };