mirror of
https://github.com/Redocly/redoc.git
synced 2025-03-10 04:45:49 +03:00
Add Virtualization to Redoc (#1)
* add virtualization to redoc * address code review, simply stuffs --------- Co-authored-by: Putu Audi Pasuatmadi <audipasuatmadi@users.noreply.github.com>
This commit is contained in:
parent
639fd2c32c
commit
2e5c98e9fc
39
package-lock.json
generated
39
package-lock.json
generated
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@cfaester/enzyme-adapter-react-18": "^0.8.0",
|
||||
"@redocly/openapi-core": "^1.4.0",
|
||||
"@tanstack/react-virtual": "^3.10.8",
|
||||
"classnames": "^2.3.2",
|
||||
"decko": "^1.2.0",
|
||||
"dompurify": "^3.0.6",
|
||||
|
@ -3623,6 +3624,31 @@
|
|||
"size-limit": "11.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
|
||||
"integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.10.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
|
||||
"integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
|
@ -21749,6 +21775,19 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@tanstack/react-virtual": {
|
||||
"version": "3.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
|
||||
"integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
|
||||
"requires": {
|
||||
"@tanstack/virtual-core": "3.10.8"
|
||||
}
|
||||
},
|
||||
"@tanstack/virtual-core": {
|
||||
"version": "3.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
|
||||
"integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA=="
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
|
|
|
@ -139,6 +139,7 @@
|
|||
"dependencies": {
|
||||
"@cfaester/enzyme-adapter-react-18": "^0.8.0",
|
||||
"@redocly/openapi-core": "^1.4.0",
|
||||
"@tanstack/react-virtual": "^3.10.8",
|
||||
"classnames": "^2.3.2",
|
||||
"decko": "^1.2.0",
|
||||
"dompurify": "^3.0.6",
|
||||
|
|
|
@ -62,6 +62,7 @@ export const RightPanel = styled.div`
|
|||
|
||||
export const DarkRightPanel = styled(RightPanel)`
|
||||
background-color: ${props => props.theme.rightPanel.backgroundColor};
|
||||
border-radius: 0.2rem;
|
||||
`;
|
||||
|
||||
export const Row = styled.div`
|
||||
|
|
|
@ -5,15 +5,15 @@ import { ThemeProvider } from '../../styled-components';
|
|||
import { OptionsProvider } from '../OptionsProvider';
|
||||
|
||||
import { AppStore } from '../../services';
|
||||
import { ApiInfo } from '../ApiInfo/';
|
||||
|
||||
import { ApiLogo } from '../ApiLogo/ApiLogo';
|
||||
import { ContentItems } from '../ContentItems/ContentItems';
|
||||
import { SideMenu } from '../SideMenu/SideMenu';
|
||||
import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar';
|
||||
import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
|
||||
|
||||
import { SearchBox } from '../SearchBox/SearchBox';
|
||||
import { StoreProvider } from '../StoreBuilder';
|
||||
import VirtualizedContent from '../Virtualization/VirtualizedContent';
|
||||
|
||||
export interface RedocProps {
|
||||
store: AppStore;
|
||||
|
@ -56,8 +56,7 @@ export class Redoc extends React.Component<RedocProps> {
|
|||
<SideMenu menu={menu} />
|
||||
</StickyResponsiveSidebar>
|
||||
<ApiContentWrap className="api-content">
|
||||
<ApiInfo store={store} />
|
||||
<ContentItems items={menu.items as any} />
|
||||
<VirtualizedContent store={store} menu={menu} />
|
||||
</ApiContentWrap>
|
||||
<BackgroundStub />
|
||||
</RedocWrap>
|
||||
|
|
91
src/components/Virtualization/VirtualizedContent.tsx
Normal file
91
src/components/Virtualization/VirtualizedContent.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
import * as React from 'react';
|
||||
import { ApiInfo, AppStore, ContentItem, ContentItemModel, MenuStore } from '../..';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import useSelectedTag from './useSelectedTag';
|
||||
import useItemReverseIndex from './useItemReverseIndex';
|
||||
|
||||
type VirtualizedContentProps = {
|
||||
store: AppStore;
|
||||
menu: MenuStore;
|
||||
};
|
||||
|
||||
/**
|
||||
* VirtualizedContent optimizes the rendering of API documentation in Redoc by virtualizing the content.
|
||||
*
|
||||
* It ensures that only the API sections currently visible within the user's viewport are rendered,
|
||||
* while off-screen sections remain unloaded until they come into view.
|
||||
* The data is still in the memory, at least the HTML doesn't have to render it which does frees
|
||||
* quite a huge amount of memory.
|
||||
*
|
||||
* This approach prevents memory issues that can arise when rendering large API documentation
|
||||
* by reducing the amount of content loaded into memory at any one time, thereby enhancing
|
||||
* performance and preventing potential crashes due to excessive memory usage.
|
||||
*
|
||||
* @author Audi
|
||||
*/
|
||||
const VirtualizedContent = ({ store, menu }: VirtualizedContentProps) => {
|
||||
const scrollableRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const renderables = React.useMemo(() => {
|
||||
return menu.flatItems;
|
||||
}, [menu.flatItems.length]);
|
||||
const { reverseIndexToVirtualIndex: reverseIndex } = useItemReverseIndex(renderables);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: renderables.length,
|
||||
getScrollElement: () => scrollableRef.current!,
|
||||
estimateSize: () => 1000,
|
||||
});
|
||||
|
||||
const selectedTag = useSelectedTag();
|
||||
|
||||
/**
|
||||
* The side effect is responsible for moving user based on the
|
||||
* selected tag into the API of choice in the virtualized view.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
const idx: number | undefined = reverseIndex[selectedTag];
|
||||
if (!idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
virtualizer.scrollToIndex(idx, {
|
||||
align: 'start',
|
||||
});
|
||||
}, [selectedTag]);
|
||||
|
||||
return (
|
||||
<div ref={scrollableRef} style={{ height: '100dvh', width: '100%', overflowY: 'auto' }}>
|
||||
<ApiInfo store={store} />
|
||||
<div
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map(virtualItem => (
|
||||
<div
|
||||
key={virtualItem.key}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
<ContentItem
|
||||
key={renderables[virtualItem.index].id}
|
||||
item={renderables[virtualItem.index] as ContentItemModel}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VirtualizedContent;
|
32
src/components/Virtualization/useItemReverseIndex.tsx
Normal file
32
src/components/Virtualization/useItemReverseIndex.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import * as React from 'react';
|
||||
import { IMenuItem } from '../..';
|
||||
|
||||
export interface MenuItemReverseIndexToVirtualIndex {
|
||||
[key: string]: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps in calculating the Reverse Index of menu items. This will pre-compute
|
||||
* the location in the virtualized index of each menu item IDs.
|
||||
*
|
||||
* The purpose is to help for faster lookup (O(1)) when user clicks the sidebar to
|
||||
* "jump" to a certain API endpoint.
|
||||
*
|
||||
* @param menuItems array of IMenuItem to create the reverse index
|
||||
* @returns key/value of id/virtualized index
|
||||
*/
|
||||
const useItemReverseIndex = (menuItems: IMenuItem[]) => {
|
||||
const reverseIndexToVirtualIndex = React.useMemo(() => {
|
||||
return menuItems.reduce(
|
||||
(prev, curr, idx) => ({ ...prev, [curr.id]: idx }),
|
||||
{} as MenuItemReverseIndexToVirtualIndex,
|
||||
);
|
||||
|
||||
// It is highly unlikely an API doc to change in runtime, so we would only
|
||||
// like to re-render if the API doc API quantity changes.
|
||||
}, [menuItems.length]);
|
||||
|
||||
return { reverseIndexToVirtualIndex: reverseIndexToVirtualIndex };
|
||||
};
|
||||
|
||||
export default useItemReverseIndex;
|
40
src/components/Virtualization/useSelectedTag.tsx
Normal file
40
src/components/Virtualization/useSelectedTag.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Redoc has a unique "tag" that serves as an anchor when user clicks
|
||||
* their sidebar.
|
||||
*
|
||||
* For example, the tag looks like "tag/myproduct-mynamespace-myendpoint".
|
||||
* It transforms a hash from the url into those of Redoc tag. For example,
|
||||
* transforms "#tag/myendpoint" into "tag/myendpoint".
|
||||
*/
|
||||
export const toRedocTag = (hash: string) => {
|
||||
return hash.substring(1, hash.length);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helps in retrieving the redoc tag user currently activates.
|
||||
* This is to help to redirect user into the associated API endpoint in the
|
||||
* Virtualization Content as the traditional HTML mechanism to redirect into anchor
|
||||
* cannot happen as not everything is rendered initially in the Virtualization Content.
|
||||
*/
|
||||
const useSelectedTag = () => {
|
||||
const [selectedTag, setSelectedTag] = React.useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
const hashCheckInterval = setInterval(() => {
|
||||
const redocTag = toRedocTag(window.location.hash);
|
||||
if (redocTag !== selectedTag) {
|
||||
setSelectedTag(redocTag);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
clearInterval(hashCheckInterval);
|
||||
};
|
||||
}, [selectedTag]);
|
||||
|
||||
return selectedTag;
|
||||
};
|
||||
|
||||
export default useSelectedTag;
|
|
@ -56,7 +56,7 @@ exports[`FieldDetailsComponent renders correctly 1`] = `
|
|||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="sc-lcIPJg sc-hknOHE gBHqkN jFBMaE"
|
||||
class="sc-esYiGF sc-kAkpmW dZoiJx gPTOUI"
|
||||
>
|
||||
<p>
|
||||
test description
|
||||
|
@ -122,7 +122,7 @@ exports[`FieldDetailsComponent renders correctly when default value is object in
|
|||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="sc-lcIPJg sc-hknOHE gBHqkN jFBMaE"
|
||||
class="sc-esYiGF sc-kAkpmW dZoiJx gPTOUI"
|
||||
>
|
||||
<p>
|
||||
test description
|
||||
|
@ -186,7 +186,7 @@ exports[`FieldDetailsComponent renders correctly when field items have string ty
|
|||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="sc-lcIPJg sc-hknOHE gBHqkN jFBMaE"
|
||||
class="sc-esYiGF sc-kAkpmW dZoiJx gPTOUI"
|
||||
>
|
||||
<p>
|
||||
test description
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SecurityRequirement should render SecurityDefs 1`] = `
|
||||
"<div id="section/Authentication/petstore_auth" data-section-id="section/Authentication/petstore_auth" class="sc-dcJsrY bBkGhy"><div class="sc-kAyceB hBQWIZ"><div class="sc-fqkvVR oJKYx"><h2 class="sc-jXbUNg fWnwAh">petstore_auth</h2><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>Get access to data while protecting your account credentials.
|
||||
"<div id="section/Authentication/petstore_auth" data-section-id="section/Authentication/petstore_auth" class="sc-jXbUNg fiwqHP"><div class="sc-dAlyuH jJfjeH"><div class="sc-imWYAI euMmdx"><h2 class="sc-cwHptR ijJbCP">petstore_auth</h2><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>Get access to data while protecting your account credentials.
|
||||
OAuth2 is also a safer and more secure way to give you access.</p>
|
||||
</div><div class="sc-ejfMa-d a-DjBE"><div class="sc-dkmUuB hFwAIA"><b>Security Scheme Type: </b><span>OAuth2</span></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Flow type: </b><code>implicit </code></div><div class="sc-dkmUuB hFwAIA"><strong> Authorization URL: </strong><code><a target="_blank" rel="noopener noreferrer" href="http://petstore.swagger.io/api/oauth/dialog">http://petstore.swagger.io/api/oauth/dialog</a></code></div><div class="sc-dkmUuB hFwAIA"><b> Scopes: </b></div><div class="sc-iEXKAA blExNw container" style="height: 4em;"><ul><li><code>write:pets</code> - <div class="sc-eeDRCY sc-eBMEME gTGgei fMmru sc-fhzFiK hXtrri redoc-markdown"><p>modify pets in your account</p>
|
||||
</div></li><li><code>read:pets</code> - <div class="sc-eeDRCY sc-eBMEME gTGgei fMmru sc-fhzFiK hXtrri redoc-markdown"><p>read your pets</p>
|
||||
</div></li></ul></div><div class="sc-EgOXT bNSpXO"></div></div></div></div></div></div><div id="section/Authentication/GitLab_PersonalAccessToken" data-section-id="section/Authentication/GitLab_PersonalAccessToken" class="sc-dcJsrY bBkGhy"><div class="sc-kAyceB hBQWIZ"><div class="sc-fqkvVR oJKYx"><h2 class="sc-jXbUNg fWnwAh">GitLab_PersonalAccessToken</h2><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>GitLab Personal Access Token description</p>
|
||||
</div><div class="sc-ejfMa-d a-DjBE"><div class="sc-dkmUuB hFwAIA"><b>Security Scheme Type: </b><span>API Key</span></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Header parameter name: </b><code>PRIVATE-TOKEN</code></div></div></div></div></div></div><div id="section/Authentication/GitLab_OpenIdConnect" data-section-id="section/Authentication/GitLab_OpenIdConnect" class="sc-dcJsrY bBkGhy"><div class="sc-kAyceB hBQWIZ"><div class="sc-fqkvVR oJKYx"><h2 class="sc-jXbUNg fWnwAh">GitLab_OpenIdConnect</h2><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>GitLab OpenIdConnect description</p>
|
||||
</div><div class="sc-ejfMa-d a-DjBE"><div class="sc-dkmUuB hFwAIA"><b>Security Scheme Type: </b><span>OpenID Connect</span></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Connect URL: </b><code><a target="_blank" rel="noopener noreferrer" href="https://gitlab.com/.well-known/openid-configuration">https://gitlab.com/.well-known/openid-configuration</a></code></div></div></div></div></div></div><div id="section/Authentication/basicAuth" data-section-id="section/Authentication/basicAuth" class="sc-dcJsrY bBkGhy"><div class="sc-kAyceB hBQWIZ"><div class="sc-fqkvVR oJKYx"><h2 class="sc-jXbUNg fWnwAh">basicAuth</h2><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"></div><div class="sc-ejfMa-d a-DjBE"><div class="sc-dkmUuB hFwAIA"><b>Security Scheme Type: </b><span>HTTP</span></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>HTTP Authorization Scheme: </b><code>basic</code></div><div class="sc-dkmUuB hFwAIA"></div></div></div></div></div></div>"
|
||||
</div><div class="sc-bVHCgj jeamQm"><div class="sc-bDpDS hgsvLV"><b>Security Scheme Type: </b><span>OAuth2</span></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Flow type: </b><code>implicit </code></div><div class="sc-bDpDS hgsvLV"><strong> Authorization URL: </strong><code><a target="_blank" rel="noopener noreferrer" href="http://petstore.swagger.io/api/oauth/dialog">http://petstore.swagger.io/api/oauth/dialog</a></code></div><div class="sc-bDpDS hgsvLV"><b> Scopes: </b></div><div class="sc-dSIIpw iEDmXP container" style="height: 4em;"><ul><li><code>write:pets</code> - <div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK sc-klVQfs cfshkX redoc-markdown"><p>modify pets in your account</p>
|
||||
</div></li><li><code>read:pets</code> - <div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK sc-klVQfs cfshkX redoc-markdown"><p>read your pets</p>
|
||||
</div></li></ul></div><div class="sc-fMMURN eKpvEA"></div></div></div></div></div></div><div id="section/Authentication/GitLab_PersonalAccessToken" data-section-id="section/Authentication/GitLab_PersonalAccessToken" class="sc-jXbUNg fiwqHP"><div class="sc-dAlyuH jJfjeH"><div class="sc-imWYAI euMmdx"><h2 class="sc-cwHptR ijJbCP">GitLab_PersonalAccessToken</h2><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>GitLab Personal Access Token description</p>
|
||||
</div><div class="sc-bVHCgj jeamQm"><div class="sc-bDpDS hgsvLV"><b>Security Scheme Type: </b><span>API Key</span></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Header parameter name: </b><code>PRIVATE-TOKEN</code></div></div></div></div></div></div><div id="section/Authentication/GitLab_OpenIdConnect" data-section-id="section/Authentication/GitLab_OpenIdConnect" class="sc-jXbUNg fiwqHP"><div class="sc-dAlyuH jJfjeH"><div class="sc-imWYAI euMmdx"><h2 class="sc-cwHptR ijJbCP">GitLab_OpenIdConnect</h2><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>GitLab OpenIdConnect description</p>
|
||||
</div><div class="sc-bVHCgj jeamQm"><div class="sc-bDpDS hgsvLV"><b>Security Scheme Type: </b><span>OpenID Connect</span></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Connect URL: </b><code><a target="_blank" rel="noopener noreferrer" href="https://gitlab.com/.well-known/openid-configuration">https://gitlab.com/.well-known/openid-configuration</a></code></div></div></div></div></div></div><div id="section/Authentication/basicAuth" data-section-id="section/Authentication/basicAuth" class="sc-jXbUNg fiwqHP"><div class="sc-dAlyuH jJfjeH"><div class="sc-imWYAI euMmdx"><h2 class="sc-cwHptR ijJbCP">basicAuth</h2><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"></div><div class="sc-bVHCgj jeamQm"><div class="sc-bDpDS hgsvLV"><b>Security Scheme Type: </b><span>HTTP</span></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>HTTP Authorization Scheme: </b><code>basic</code></div><div class="sc-bDpDS hgsvLV"></div></div></div></div></div></div>"
|
||||
`;
|
||||
|
||||
exports[`SecurityRequirement should render authDefinition 1`] = `"<div class="sc-bDumWk iWBBny"><div class="sc-sLsrZ hgeUJn"><h5 class="sc-dAlyuH sc-fifgRP jbQuod kWJur">Authorizations:</h5><svg class="sc-cwHptR iZRiKW" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-dBmzty eoFcYg"><span class="sc-kbousE cpXQuZ">(<span class="sc-gfoqjT kbvnry">API Key: <i>GitLab_PersonalAccessToken</i></span><span class="sc-gfoqjT kbvnry">OpenID Connect: <i>GitLab_OpenIdConnect</i></span><span class="sc-gfoqjT kbvnry">HTTP: <i>basicAuth</i></span>) </span><span class="sc-kbousE cpXQuZ"><span class="sc-gfoqjT kbvnry">OAuth2: <i>petstore_auth</i></span></span></div></div>,"`;
|
||||
exports[`SecurityRequirement should render authDefinition 1`] = `"<div class="sc-kzqdkY iBGSSi"><div class="sc-kWtpeL btBUKm"><h5 class="sc-dLMFU sc-gdyeKB lcezuG gSXcBp">Authorizations:</h5><svg class="sc-gsFSXq cOXSQA" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-ecPEgm cOTqGl"><span class="sc-hHOBiw jggLOL">(<span class="sc-dlWCHZ dCGAkl">API Key: <i>GitLab_PersonalAccessToken</i></span><span class="sc-dlWCHZ dCGAkl">OpenID Connect: <i>GitLab_OpenIdConnect</i></span><span class="sc-dlWCHZ dCGAkl">HTTP: <i>basicAuth</i></span>) </span><span class="sc-hHOBiw jggLOL"><span class="sc-dlWCHZ dCGAkl">OAuth2: <i>petstore_auth</i></span></span></div></div>,"`;
|
||||
|
||||
exports[`SecurityRequirement should render authDefinition 2`] = `
|
||||
"<div class="sc-bDumWk gtsPcy"><div class="sc-sLsrZ hgeUJn"><h5 class="sc-dAlyuH sc-fifgRP jbQuod kWJur">Authorizations:</h5><svg class="sc-cwHptR dSJqIk" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-dBmzty llvZdI"><span class="sc-kbousE dOwJQz">(<span class="sc-gfoqjT kbvnry">API Key: <i>GitLab_PersonalAccessToken</i></span><span class="sc-gfoqjT kbvnry">OpenID Connect: <i>GitLab_OpenIdConnect</i></span><span class="sc-gfoqjT kbvnry">HTTP: <i>basicAuth</i></span>) </span><span class="sc-kbousE dOwJQz"><span class="sc-gfoqjT kbvnry">OAuth2: <i>petstore_auth</i> (<code class="sc-eyvILC bzHwfc">write:pets</code><code class="sc-eyvILC bzHwfc">read:pets</code>) </span></span></div></div><div class="sc-ejfMa-d a-DjBE"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> OAuth2: petstore_auth</h5><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>Get access to data while protecting your account credentials.
|
||||
"<div class="sc-kzqdkY ftTbsm"><div class="sc-kWtpeL btBUKm"><h5 class="sc-dLMFU sc-gdyeKB lcezuG gSXcBp">Authorizations:</h5><svg class="sc-gsFSXq eWnmLm" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-ecPEgm fYRInh"><span class="sc-hHOBiw bWBltB">(<span class="sc-dlWCHZ dCGAkl">API Key: <i>GitLab_PersonalAccessToken</i></span><span class="sc-dlWCHZ dCGAkl">OpenID Connect: <i>GitLab_OpenIdConnect</i></span><span class="sc-dlWCHZ dCGAkl">HTTP: <i>basicAuth</i></span>) </span><span class="sc-hHOBiw bWBltB"><span class="sc-dlWCHZ dCGAkl">OAuth2: <i>petstore_auth</i> (<code class="sc-eZYNyq eZCLxU">write:pets</code><code class="sc-eZYNyq eZCLxU">read:pets</code>) </span></span></div></div><div class="sc-bVHCgj jeamQm"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> OAuth2: petstore_auth</h5><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>Get access to data while protecting your account credentials.
|
||||
OAuth2 is also a safer and more secure way to give you access.</p>
|
||||
</div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Flow type: </b><code>implicit </code></div><div class="sc-dkmUuB hFwAIA"><strong> Authorization URL: </strong><code><a target="_blank" rel="noopener noreferrer" href="http://petstore.swagger.io/api/oauth/dialog">http://petstore.swagger.io/api/oauth/dialog</a></code></div><div><b>Required scopes: </b><code>write:pets</code> <code>read:pets</code> </div><div class="sc-dkmUuB hFwAIA"><b> Scopes: </b></div><div class="sc-iEXKAA blExNw container" style="height: 4em;"><ul><li><code>write:pets</code> - <div class="sc-eeDRCY sc-eBMEME gTGgei fMmru sc-fhzFiK hXtrri redoc-markdown"><p>modify pets in your account</p>
|
||||
</div></li><li><code>read:pets</code> - <div class="sc-eeDRCY sc-eBMEME gTGgei fMmru sc-fhzFiK hXtrri redoc-markdown"><p>read your pets</p>
|
||||
</div></li></ul></div><div class="sc-EgOXT bNSpXO"></div></div></div><div class="sc-ejfMa-d a-DjBE"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> API Key: GitLab_PersonalAccessToken</h5><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>GitLab Personal Access Token description</p>
|
||||
</div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Header parameter name: </b><code>PRIVATE-TOKEN</code></div></div></div><div class="sc-ejfMa-d a-DjBE"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> OpenID Connect: GitLab_OpenIdConnect</h5><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>GitLab OpenIdConnect description</p>
|
||||
</div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Connect URL: </b><code><a target="_blank" rel="noopener noreferrer" href="https://gitlab.com/.well-known/openid-configuration">https://gitlab.com/.well-known/openid-configuration</a></code></div></div></div><div class="sc-ejfMa-d a-DjBE"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> HTTP: basicAuth</h5><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>HTTP Authorization Scheme: </b><code>basic</code></div><div class="sc-dkmUuB hFwAIA"></div></div></div>,"
|
||||
</div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Flow type: </b><code>implicit </code></div><div class="sc-bDpDS hgsvLV"><strong> Authorization URL: </strong><code><a target="_blank" rel="noopener noreferrer" href="http://petstore.swagger.io/api/oauth/dialog">http://petstore.swagger.io/api/oauth/dialog</a></code></div><div><b>Required scopes: </b><code>write:pets</code> <code>read:pets</code> </div><div class="sc-bDpDS hgsvLV"><b> Scopes: </b></div><div class="sc-dSIIpw iEDmXP container" style="height: 4em;"><ul><li><code>write:pets</code> - <div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK sc-klVQfs cfshkX redoc-markdown"><p>modify pets in your account</p>
|
||||
</div></li><li><code>read:pets</code> - <div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK sc-klVQfs cfshkX redoc-markdown"><p>read your pets</p>
|
||||
</div></li></ul></div><div class="sc-fMMURN eKpvEA"></div></div></div><div class="sc-bVHCgj jeamQm"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> API Key: GitLab_PersonalAccessToken</h5><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>GitLab Personal Access Token description</p>
|
||||
</div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Header parameter name: </b><code>PRIVATE-TOKEN</code></div></div></div><div class="sc-bVHCgj jeamQm"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> OpenID Connect: GitLab_OpenIdConnect</h5><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>GitLab OpenIdConnect description</p>
|
||||
</div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Connect URL: </b><code><a target="_blank" rel="noopener noreferrer" href="https://gitlab.com/.well-known/openid-configuration">https://gitlab.com/.well-known/openid-configuration</a></code></div></div></div><div class="sc-bVHCgj jeamQm"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> HTTP: basicAuth</h5><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>HTTP Authorization Scheme: </b><code>basic</code></div><div class="sc-bDpDS hgsvLV"></div></div></div>,"
|
||||
`;
|
||||
|
|
100
src/components/__tests__/useItemReverseIndex.test.tsx
Normal file
100
src/components/__tests__/useItemReverseIndex.test.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom/client';
|
||||
import { IMenuItem } from '../..';
|
||||
import useItemReverseIndex, {
|
||||
MenuItemReverseIndexToVirtualIndex,
|
||||
} from '../Virtualization/useItemReverseIndex';
|
||||
|
||||
(global as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
describe('useItemReverseIndex', () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: ReactDOM.Root;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
React.act(() => root.unmount());
|
||||
document.body.removeChild(container);
|
||||
container = null as any;
|
||||
});
|
||||
|
||||
it('it should maps item based on the id for quick lookup', () => {
|
||||
const menuItems: any[] = [
|
||||
{ id: 'item1', name: 'Item 1' },
|
||||
{ id: 'item2', name: 'Item 2' },
|
||||
{ id: 'item3', name: 'Item 3' },
|
||||
];
|
||||
|
||||
let result: MenuItemReverseIndexToVirtualIndex | undefined;
|
||||
|
||||
// Hooks can only be used inside a React component.
|
||||
// This component is just to host the hook.
|
||||
const TestComponent = ({ items }: { items: IMenuItem[] }) => {
|
||||
const { reverseIndexToVirtualIndex } = useItemReverseIndex(items);
|
||||
result = reverseIndexToVirtualIndex; // this is to capture the hook's output for later's assertions
|
||||
return null;
|
||||
};
|
||||
|
||||
// Render component within act to handle React state updates
|
||||
React.act(() => {
|
||||
root.render(<TestComponent items={menuItems} />);
|
||||
});
|
||||
|
||||
const expectedMapping: MenuItemReverseIndexToVirtualIndex = {
|
||||
item1: 0,
|
||||
item2: 1,
|
||||
item3: 2,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedMapping);
|
||||
});
|
||||
|
||||
// Note: the test below only tests when the items change in quantity.
|
||||
// This is because it is very unlikely for an API docs to change in the first-place,
|
||||
// so just-in-case, I only allow it to re-render when the items change in quantity.
|
||||
it('should update the mapping when menu items change in quantity', () => {
|
||||
let result: MenuItemReverseIndexToVirtualIndex | undefined;
|
||||
|
||||
const initialItems: any[] = [
|
||||
{ id: 'item1', description: 'Item 1' },
|
||||
{ id: 'item2', description: 'Item 2' },
|
||||
];
|
||||
|
||||
const newItems: any[] = [
|
||||
{ id: 'newItem1', description: 'New Item 1' },
|
||||
{ id: 'newItem2', description: 'New Item 2' },
|
||||
{ id: 'newItem3', description: 'New Item 3' },
|
||||
];
|
||||
|
||||
// Hooks can only be used inside a React component.
|
||||
// This component is just to host the hook.
|
||||
const TestComponent = ({ items }: { items: IMenuItem[] }) => {
|
||||
const { reverseIndexToVirtualIndex } = useItemReverseIndex(items);
|
||||
result = reverseIndexToVirtualIndex;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Initial render
|
||||
React.act(() => {
|
||||
root.render(<TestComponent items={initialItems} />);
|
||||
});
|
||||
|
||||
// Update render with new items
|
||||
React.act(() => {
|
||||
root.render(<TestComponent items={newItems} />);
|
||||
});
|
||||
|
||||
const expectedNewMapping: MenuItemReverseIndexToVirtualIndex = {
|
||||
newItem1: 0,
|
||||
newItem2: 1,
|
||||
newItem3: 2,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedNewMapping);
|
||||
});
|
||||
});
|
134
src/components/__tests__/useSelectedTag.test.tsx
Normal file
134
src/components/__tests__/useSelectedTag.test.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom/client';
|
||||
import useSelectedTag from '../Virtualization/useSelectedTag';
|
||||
|
||||
(global as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('useSelectedTag', () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: ReactDOM.Root;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = ReactDOM.createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
React.act(() => root.unmount());
|
||||
document.body.removeChild(container);
|
||||
container = null as any;
|
||||
jest.clearAllTimers();
|
||||
});
|
||||
|
||||
it('should return the correct tag based on the hash in the URL', async () => {
|
||||
window.location.hash = '#tag/product-namespace-verb';
|
||||
|
||||
let selectedTag: string | undefined;
|
||||
|
||||
// Hooks can only be used inside a React component.
|
||||
// This component is just to host the hook.
|
||||
const TestComponent = () => {
|
||||
selectedTag = useSelectedTag();
|
||||
return <p data-testid={selectedTag}>test</p>;
|
||||
};
|
||||
|
||||
React.act(() => {
|
||||
root.render(<TestComponent />);
|
||||
});
|
||||
|
||||
await React.act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
|
||||
expect(selectedTag).toBe('tag/product-namespace-verb');
|
||||
});
|
||||
|
||||
it('resetting a tag will also reset the selected tag from the hook', async () => {
|
||||
let selectedTag: string | undefined;
|
||||
|
||||
// Hooks can only be used inside a React component.
|
||||
// This component is just to host the hook.
|
||||
const TestComponent = () => {
|
||||
selectedTag = useSelectedTag();
|
||||
return <p data-testid={selectedTag}>test</p>;
|
||||
};
|
||||
|
||||
React.act(() => {
|
||||
root.render(<TestComponent />);
|
||||
});
|
||||
|
||||
expect(selectedTag).toBe('');
|
||||
|
||||
window.location.hash = '#tag/product-namespace-verb';
|
||||
await React.act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
expect(selectedTag).toBe('tag/product-namespace-verb');
|
||||
|
||||
window.location.hash = '';
|
||||
await React.act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
expect(selectedTag).toBe('');
|
||||
});
|
||||
|
||||
it('should update the selected tag when hash changes in the URL', async () => {
|
||||
window.location.hash = '#tag/product-namespace-verb';
|
||||
|
||||
let selectedTag: string | undefined;
|
||||
|
||||
// Hooks can only be used inside a React component.
|
||||
// This component is just to host the hook.
|
||||
const TestComponent = () => {
|
||||
selectedTag = useSelectedTag();
|
||||
return null;
|
||||
};
|
||||
|
||||
React.act(() => {
|
||||
root.render(<TestComponent />);
|
||||
});
|
||||
|
||||
await React.act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
expect(selectedTag).toBe('tag/product-namespace-verb');
|
||||
|
||||
window.location.hash =
|
||||
'#tag/product-namespace-verb/operation/product-namespace-verb_OperationID';
|
||||
await React.act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
expect(selectedTag).toBe(
|
||||
'tag/product-namespace-verb/operation/product-namespace-verb_OperationID',
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear the interval on component unmount', () => {
|
||||
const clearIntervalSpy = jest.spyOn(window, 'clearInterval');
|
||||
|
||||
// Hooks can only be used inside a React component.
|
||||
// This component is just to host the hook.
|
||||
const TestComponent = () => {
|
||||
useSelectedTag();
|
||||
return null;
|
||||
};
|
||||
|
||||
React.act(() => {
|
||||
root.render(<TestComponent />);
|
||||
});
|
||||
|
||||
// Ensure the component is mounted
|
||||
expect(clearIntervalSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Unmount the component
|
||||
React.act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
|
||||
expect(clearIntervalSpy).toHaveBeenCalledTimes(1);
|
||||
clearIntervalSpy.mockRestore();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user