Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swagger UI #805

Open
wants to merge 54 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
6971ef6
preliminary tests
aurel-l Jan 17, 2024
8d1cffd
merge main
aurel-l Jan 17, 2024
6c1568c
Merge remote-tracking branch 'origin/main' into swagger-ui
dlrice Jul 17, 2024
b69e801
Create DocumenationHeader and selectively show in BaseLayout.
dlrice Jul 17, 2024
cd3fcdb
Replace swagger-ui with swagger-ui-react.
dlrice Jul 17, 2024
8695523
Update webpack for swagger-ui-react css file.
dlrice Jul 17, 2024
992369d
Add api doc config, types and styles.
dlrice Jul 17, 2024
3e91031
Navigate on definition selection.
dlrice Jul 17, 2024
73f2ab5
Wire up definition selection to loading of API swagger file.
dlrice Jul 17, 2024
589cfe9
Add openapi-types package.
dlrice Jul 18, 2024
dd87462
Use AugmentingLayout; add TOC.
dlrice Jul 18, 2024
a27618a
Open corresponding section when anchor tag preset.
dlrice Jul 18, 2024
a87724d
Create and use getIdToOperation fn.
dlrice Jul 18, 2024
9d58853
Style tweak.
dlrice Jul 18, 2024
81eebe9
Change default OperationTag presentation.
dlrice Jul 18, 2024
6db5c18
Lint fixes.
dlrice Jul 18, 2024
d6eff69
Generate TOC from spec.
dlrice Jul 18, 2024
63a8ba4
Use id rather than label for inpagenav item key.
dlrice Jul 18, 2024
8df3a34
Add supporting data api docs endpoint.
dlrice Jul 19, 2024
2598a01
Update AA label.
dlrice Jul 19, 2024
c77e2cd
Adapt swagger-ui to uniprot styles.
dlrice Jul 19, 2024
b8e1ce0
Wording tweak.
dlrice Jul 19, 2024
2cf131b
Tab approach.
dlrice Jul 19, 2024
ae5dffe
Close other sections upon root click; style and code tweaks.
dlrice Jul 22, 2024
b84840b
Point to production rest.uniprot.org for api docs.
dlrice Jul 22, 2024
e520e42
Fix api doc endpoint paths.
dlrice Jul 22, 2024
77c774a
Update POST style; clean up comments.
dlrice Jul 24, 2024
20295d9
Tidy up. Rename Documentation to ApiDocumentation.
dlrice Jul 24, 2024
63964e5
Add unirefOpenApi mock.
dlrice Jul 24, 2024
74bbdea
Remove uniprotkb-open-api.json.
dlrice Jul 24, 2024
101018a
Extract fns into own util file and test.
dlrice Jul 25, 2024
91f4f36
dangerouslySetInnerHTML for operation tag content.
dlrice Jul 25, 2024
9677943
Add todo/jira for code snippets.
dlrice Jul 25, 2024
2a24f12
Comment edit.
dlrice Jul 25, 2024
722c079
fix linting issue
aurel-l Jul 29, 2024
5ef88e6
Update src/help/utils/__tests__/apiDocumentation.spec.tsx
dlrice Jul 29, 2024
818bd42
Merge remote-tracking branch 'origin/main' into swagger-ui
dlrice Sep 10, 2024
db5c643
Address PR comments and redirect on invalid definition tab in url.
dlrice Sep 10, 2024
91f4874
Fix Schemas td vertical alignment.
dlrice Sep 10, 2024
878086a
Add schemas link to in page nav.
dlrice Sep 11, 2024
ed9990d
Mock swagger-ui-react in tests.
dlrice Sep 11, 2024
aa00e08
Remove circular dependency.
dlrice Sep 11, 2024
1b07353
change way to add ID to the swagger schema/model
aurel-l Sep 19, 2024
901c1a0
add code snippets to swagger
aurel-l Sep 19, 2024
2eebe1b
prevent blowing up browser when using Swagger "try it out" on stream …
aurel-l Sep 20, 2024
21064fb
Apply suggestions from code review
aurel-l Sep 20, 2024
8316264
address PR comments
aurel-l Sep 20, 2024
2989cfd
Merge branch 'swagger-ui' of https://github.com/ebi-uniprot/uniprot-w…
aurel-l Sep 20, 2024
66da708
update snapshots
aurel-l Sep 20, 2024
b41eecc
Update src/help/utils/apiSnippets.tsx
aurel-l Sep 30, 2024
6d6a060
update snapshots
aurel-l Sep 30, 2024
9df0318
update R snippet to httr2
aurel-l Sep 30, 2024
becbab5
update comments in snippets
aurel-l Sep 30, 2024
7c242c2
Merge branch 'main' into swagger-ui
Swaathik Oct 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"react-router-dom": "5.3.4",
"sanitize-html": "2.7.3",
"schema-dts": "1.1.0",
"swagger-ui-react": "5.17.14",
"timing-functions": "2.0.1",
"tippy.js": "6.3.7",
"type-fest": "3.1.0",
Expand Down Expand Up @@ -179,6 +180,7 @@
"@types/react-dom": "18.2.0",
"@types/react-router-dom": "5.3.3",
"@types/sanitize-html": "2.6.2",
"@types/swagger-ui-react": "^4.18.3",
"@types/url-join": "4.0.1",
"@types/uuid": "8.3.4",
"@typescript-eslint/eslint-plugin": "5.42.0",
Expand Down Expand Up @@ -216,6 +218,7 @@
"mini-css-extract-plugin": "2.6.1",
"node-sass-json-importer": "4.3.0",
"npm-run-all": "4.1.5",
"openapi-types": "12.1.3",
"prettier": "2.7.1",
"process": "0.11.10",
"react-test-renderer": "18.2.0",
Expand Down
10 changes: 10 additions & 0 deletions src/app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ const HelpResults = lazy(
/* webpackChunkName: "help-results" */ '../../help/components/results/Results'
)
);
const ApiDocumentationPage = lazy(
() =>
import(
/* webpackChunkName: "documentation" */ '../../help/components/entry/ApiDocumentation'
)
);

// Contact
const ContactForm = lazy(
Expand Down Expand Up @@ -546,6 +552,10 @@ const App = () => {
path={LocationToPath[Location.ReleaseNotesResults]}
component={HelpResults}
/>
<Route
path={LocationToPath[Location.Documentation]}
component={ApiDocumentationPage}
/>
{/* Contact */}
<Route
path={LocationToPath[Location.ContactGeneric]}
Expand Down
2 changes: 2 additions & 0 deletions src/app/config/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export enum Location {
// Help
HelpEntry = 'HelpEntry',
HelpResults = 'HelpResults',
Documentation = 'Documentation',
// Release Notes
ReleaseNotesEntry = 'ReleaseNotesEntry',
ReleaseNotesResults = 'ReleaseNotesResults',
Expand Down Expand Up @@ -131,6 +132,7 @@ export const LocationToPath: Record<Location, string> = {
// Help
[Location.HelpEntry]: '/help/:accession',
[Location.HelpResults]: '/help',
[Location.Documentation]: '/documentation/:definition?',
// News
[Location.ReleaseNotesEntry]: '/release-notes/:accession+',
[Location.ReleaseNotesResults]: '/release-notes',
Expand Down
57 changes: 57 additions & 0 deletions src/help/components/entry/ApiDocumentation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Tabs, Tab } from 'franklin-sites';
import { useEffect } from 'react';
import {
generatePath,
Link,
useHistory,
useRouteMatch,
} from 'react-router-dom';

import DocumentationTab from './ApiDocumentationTab';

import { LocationToPath, Location } from '../../../app/config/urls';
import { apiDocsDefinitionToString } from '../../config/apiDocumentation';

import { ApiDocsDefinition } from '../../types/apiDocumentation';

import 'swagger-ui-react/swagger-ui.css';

const ApiDocumentation = () => {
const history = useHistory();
const match = useRouteMatch<{ definition: ApiDocsDefinition }>(
LocationToPath[Location.Documentation]
);
const definition = match?.params.definition;
useEffect(() => {
if (!definition) {
history.replace({
pathname: generatePath(LocationToPath[Location.Documentation], {
definition: ApiDocsDefinition.uniprotkb,
}),
});
}
}, [definition, history]);
return (
<Tabs active={definition}>
{Array.from(apiDocsDefinitionToString).map(([id, label]) => (
<Tab
title={
<Link
to={generatePath(LocationToPath[Location.Documentation], {
definition: id,
})}
>
{label}
</Link>
}
id={id}
key={id}
>
<DocumentationTab />
aurel-l marked this conversation as resolved.
Show resolved Hide resolved
</Tab>
))}
</Tabs>
);
};

export default ApiDocumentation;
171 changes: 171 additions & 0 deletions src/help/components/entry/ApiDocumentationTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { Location as HistoryLocation } from 'history';
import { Card, Loader } from 'franklin-sites';
import SwaggerUI from 'swagger-ui-react';
import { frame } from 'timing-functions';
import type { OpenAPIV3 } from 'openapi-types';

import HTMLHead from '../../../shared/components/HTMLHead';
import ErrorBoundary from '../../../shared/components/error-component/ErrorBoundary';
import ErrorHandler from '../../../shared/components/error-pages/ErrorHandler';
import { SidebarLayout } from '../../../shared/components/layouts/SideBarLayout';
import InPageNav from '../../../shared/components/InPageNav';

import useDataApi from '../../../shared/hooks/useDataApi';

import {
getIdToOperation,
getLayoutAction,
getTagIdsAndSections,
tagNameToId,
} from '../../utils/apiDocumentation';

import { LocationToPath, Location } from '../../../app/config/urls';
import apiUrls from '../../config/apiUrls';

import { ApiDocsDefinition } from '../../types/apiDocumentation';

import 'swagger-ui-react/swagger-ui.css';
import styles from './styles/api-documentation.module.scss';

const OperationTag = ({
tagObj,
children,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tagObj: any;
children: ReactNode;
}) => {
const tagDetails = tagObj.get('tagDetails');
return (
<div className={styles['operation-tag']}>
<h1 id={tagNameToId(tagDetails.get('name'))} className="medium">
{tagDetails.get('name')}
</h1>
{/* eslint-disable-next-line react/no-danger */}
<p dangerouslySetInnerHTML={{ __html: tagDetails.get('description') }} />
<hr />
{children}
</div>
);
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const AugmentingLayout = ({ getComponent, dispatch, spec: getSpec }: any) => {
const history = useHistory();
const BaseLayout = getComponent('BaseLayout', true);
const [tagIds, sections, idToOperation] = useMemo(() => {
const spec = getSpec().get('json');
const idToOperation = getIdToOperation(spec.get('paths').toJSON());
return [
...getTagIdsAndSections(spec, idToOperation, styles['section-path']),
idToOperation,
];
}, [getSpec]);
const openOperationAtLocation = useCallback(
(location: HistoryLocation) => {
const id = location.hash.replace('#', '');
if (tagIds.has(id)) {
for (const operation of idToOperation.values()) {
dispatch(getLayoutAction(operation, false));
}
} else {
const operation = idToOperation.get(id);
if (operation?.tag && operation?.operationId) {
dispatch(getLayoutAction(operation, true));
}
}
},
[dispatch, idToOperation, tagIds]
);

useEffect(() => {
const unlisten = history.listen((location) =>
frame().then(() => openOperationAtLocation(location))
);
openOperationAtLocation(history.location);
return unlisten;
}, [history, openOperationAtLocation]);

return (
<SidebarLayout
className={styles.wider}
sidebar={
<div className={styles.sidebar}>
<InPageNav sections={sections} />
</div>
}
>
<HTMLHead title="UniProt website API documentation">
<meta name="robots" content="noindex" />
</HTMLHead>
<Card className={styles.content}>
<ErrorBoundary>
<BaseLayout />
</ErrorBoundary>
</Card>
</SidebarLayout>
);
};

const AugmentingLayoutPlugin = () => ({
components: {
AugmentingLayout,
Schemes: () => null,
InfoContainer: () => null,
ServersContainer: () => null,
OperationTag,
},
});

const ApiDocumentationTab = () => {
const match = useRouteMatch<{ definition: ApiDocsDefinition }>(
LocationToPath[Location.Documentation]
);
const definition = match?.params.definition;

const data = useDataApi<OpenAPIV3.Document>(
definition && apiUrls.apiDocumnentationDefinition(definition)
);

if (data.loading) {
return <Loader progress={data.progress} />;
}

if (data.error || !data.data) {
return <ErrorHandler status={data.status} error={data.error} fullPage />;
}

// TODO: enable request snippets eg python. https://www.ebi.ac.uk/panda/jira/browse/TRM-31649
// Leaving this config here in case it is useful in the future:
// requestSnippets: {
// generators: {
// curl_bash: {
// title: 'cURL (bash)',
// syntax: 'bash',
// },
// curl_powershell: {
// title: 'cURL (PowerShell)',
// syntax: 'powershell',
// },
// curl_cmd: {
// title: 'cURL (CMD)',
// syntax: 'bash',
// },
// python_requests: {
// title: 'Python (requests)',
// syntax: 'python',
// },
// },

return !definition ? null : (
<SwaggerUI
spec={data.data}
plugins={[AugmentingLayoutPlugin]}
layout="AugmentingLayout"
/>
);
};

export default ApiDocumentationTab;
76 changes: 76 additions & 0 deletions src/help/components/entry/styles/api-documentation.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@import 'franklin-sites/src/styles/colours';
@import 'franklin-sites/src/styles/settings';
@import 'franklin-sites/src/styles/mixins';

.wider {
grid-template-columns: 3rem calc(1.3 * var(--sidebar-size) - 3rem) minmax(
min-content,
1fr
);
}
aurel-l marked this conversation as resolved.
Show resolved Hide resolved

.sidebar {
.section-path {
margin-left: 0.75rem;
}
}

.content {
.operation-tag {
&:not(:first-child) {
margin-top: 3rem;
}
h1 {
margin: 1rem 0;
}
p {
margin-bottom: 0.5rem;
}

hr {
border: none;
height: 0.125rem;
background-color: $colour-platinum;
margin-bottom: 0.5rem;
}
}

:global(.swagger-ui) {
:global(.scheme-container) {
display: none;
}

:global(.opblock) {
border-radius: 0px;
}

:global(.opblock.opblock-get),
:global(.opblock.opblock-post) {
border: 0px;
:global(.arrow) {
fill: white;
}

:global(.opblock-summary) {
border: 0px;
background-color: $colour-sea-blue;
}
:global(.opblock-summary-method) {
border-radius: 0px;
}

:global(.opblock-summary-path),
:global(.opblock-summary-description) {
color: white;
}
}

:global(.opblock.opblock-get) :global(.opblock-summary-method) {
background: lighten($colour-sea-blue, 10%);
}

// :global(.opblock.opblock-post) :global(.opblock-summary-method) {
// background: mediumseagreen;
// }
}
}
12 changes: 12 additions & 0 deletions src/help/config/apiDocumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiDocsDefinition } from '../types/apiDocumentation';

export const apiDocsDefinitionToString = new Map([
[ApiDocsDefinition.uniprotkb, 'UniProtKB'],
[ApiDocsDefinition.uniref, 'UniRef'],
[ApiDocsDefinition.uniparc, 'UniParc'],
[ApiDocsDefinition.proteomes, 'Proteomes'],
[ApiDocsDefinition.support_data, 'Supporting Data'],
[ApiDocsDefinition.aa, 'Automatic Annotation'],
[ApiDocsDefinition.idmapping, 'ID Mapping'],
[ApiDocsDefinition.async_download, 'Async Download'],
]);
Loading