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:
Putu Audi Pasuatmadi 2024-11-08 12:42:20 +08:00 committed by GitHub
parent 639fd2c32c
commit 2e5c98e9fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 457 additions and 20 deletions

39
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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`

View File

@ -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>

View 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;

View 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;

View 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;

View File

@ -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

View File

@ -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>,"
`;

View 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);
});
});

View 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();
});
});