Skip to content

Commit

Permalink
OpenAPI visualization Tool (#752)
Browse files Browse the repository at this point in the history
* Add OpenAPI UI into the server tab to enable local test from server to service 

---------

Co-authored-by: Liangying.Wei <[email protected]>
  • Loading branch information
jiangy10 and vicancy authored Jul 31, 2024
1 parent bbe43c1 commit f209560
Show file tree
Hide file tree
Showing 13 changed files with 622 additions and 43 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
- run: |
cd tools/awps-tunnel/client
yarn install
yarn run build
yarn test
- run: yarn workspaces run test
- name: Setup .NET ${{ matrix.dotnet-version }}
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -367,5 +367,5 @@ MigrationBackup/
env/
website/docusaurus/

tools/awps-tunnel/client/public/examples/
tools/awps-tunnel/client/src/components/api/restapiSample.json
# Azure Web PubSub tunnel API visulization tool resources
tools/awps-tunnel/client/public/api/
12 changes: 8 additions & 4 deletions tools/awps-tunnel/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@fluentui/react": "^8.106.3",
"@fluentui/react-components": "^9.34.1",
"@microsoft/signalr": "^8.0.0",
"@monaco-editor/react": "^4.6.0",
"@popperjs/core": "2.11.8",
"axios": "^1.7.2",
"bootstrap": "^5.1.3",
Expand All @@ -19,6 +20,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-highlight": "^0.15.0",
"react-json-editor-ajrm": "2.5.13",
"react-json-view": "^1.21.3",
"react-markdown": "^9.0.0",
"react-router-bootstrap": "^0.26.1",
Expand Down Expand Up @@ -53,11 +55,13 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^29.5.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/markdown-it": "^13.0.2",
"@types/node": "^16.18.53",
"@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7",
"@types/react-highlight": "^0.12.8",
"@types/react-json-editor-ajrm": "^2.5.6",
"@typescript-eslint/typescript-estree": "^6.12.0",
"ajv": "^8.11.0",
"cross-env": "^7.0.3",
Expand All @@ -76,12 +80,12 @@
},
"scripts": {
"clean": "rimraf ./build",
"start": "cross-env REACT_APP_DATA_FETCHER=mock react-scripts start",
"start": "cross-env REACT_APP_DATA_FETCHER=mock REACT_APP_API_VERSION=2023-07-01 react-scripts start",
"prebuild": "node scripts/downloadExamples.js",
"build": "npm run prebuild && npm run build:mock",
"build:npm": "cross-env REACT_APP_DATA_FETCHER=npm react-scripts build",
"build:mock": "cross-env REACT_APP_DATA_FETCHER=mock react-scripts build",
"test": "cross-env CI=true REACT_APP_DATA_FETCHER=mock react-scripts test",
"build:npm": "cross-env REACT_APP_DATA_FETCHER=npm REACT_APP_API_VERSION=2023-07-01 react-scripts build",
"build:mock": "cross-env REACT_APP_DATA_FETCHER=mock REACT_APP_API_VERSION=2023-07-01 react-scripts build",
"test": "cross-env CI=true REACT_APP_DATA_FETCHER=mock REACT_APP_API_VERSION=2023-07-01 react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint ./src/"
},
Expand Down
6 changes: 3 additions & 3 deletions tools/awps-tunnel/client/scripts/downloadExamples.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ const axios = require('axios');
const fs = require('fs');
const path = require('path');

const examplesDir = path.join(__dirname, '../public/api/examples');
const apiDir = path.join(__dirname, '../src/components/api');
const version = process.argv[2] || '2023-07-01';
const examplesDir = path.join(__dirname, `../public/api/${version}/examples/`);
const apiDir = path.join(__dirname, `../public/api/${version}`);

if (!fs.existsSync(examplesDir)) {
fs.mkdirSync(examplesDir, { recursive: true });
Expand Down Expand Up @@ -35,7 +35,7 @@ async function downloadFiles() {
const apiUrl = `https://api.github.com/repos/${repoPath}/contents/${apiFilePath}?ref=${ref}`;
const apiResponse = await axios.get(apiUrl, { responseType: 'json' });
const apiFileResponse = await axios.get(apiResponse.data.download_url, { responseType: 'arraybuffer' });
fs.writeFileSync(path.join(apiDir, 'restapiSample.json'), apiFileResponse.data);
fs.writeFileSync(path.join(apiDir, apiResponse.data.name), apiFileResponse.data);
console.log('api spec downloaded successfully.');
} catch (error) {
console.error('Error downloading files:', error);
Expand Down
92 changes: 92 additions & 0 deletions tools/awps-tunnel/client/src/components/api/EndpointNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
Accordion,
AccordionHeader,
AccordionItem,
AccordionPanel,
Label,
Tab,
TabList
} from "@fluentui/react-components";
import React, { useEffect, useState } from "react";
import { PathItem } from "../../models";
import { methodColors } from './Methods';
import { useDataContext } from "../../providers/DataContext";

export function EndpointNav({ setSelectedPath }: {
setSelectedPath: React.Dispatch<React.SetStateAction<string | undefined>>
}): React.JSX.Element {
const { data } = useDataContext();
const [categories, setCategories] = useState<{
general: { pathUrl: string, path: PathItem }[];
groups: { pathUrl: string, path: PathItem }[];
connections: { pathUrl: string, path: PathItem }[];
users: { pathUrl: string, path: PathItem }[];
permissions: { pathUrl: string, path: PathItem }[];
}>();

useEffect(() => {
let categories: {
general: { pathUrl: string, path: PathItem }[],
groups: { pathUrl: string, path: PathItem }[],
connections: { pathUrl: string, path: PathItem }[],
users: { pathUrl: string, path: PathItem }[],
permissions: { pathUrl: string, path: PathItem }[]
} = {
general: [],
groups: [],
connections: [],
users: [],
permissions: []
};
Object.entries(data.apiSpec.paths).forEach(([pathUrl, path]) => {
const segments = pathUrl.split('/');
const category = segments[4] || 'general'; // Default to 'general' if no fourth segment
switch (category) {
case 'groups':
categories.groups.push({ pathUrl: pathUrl, path: path as PathItem });
break;
case 'connections':
categories.connections.push({ pathUrl: pathUrl, path: path as PathItem });
break;
case 'users':
categories.users.push({ pathUrl: pathUrl, path: path as PathItem });
break;
case 'permissions':
categories.permissions.push({ pathUrl: pathUrl, path: path as PathItem });
break;
default:
categories.general.push({ pathUrl: pathUrl, path: path as PathItem });
break;
}
});
setCategories(categories);
}, [data.apiSpec]);

return (<div className="d-flex overflow-hidden" style={{ flex: 1 }}>
<TabList onTabSelect={(_e, data) => {
setSelectedPath(data.value as string)
}} vertical>
<Accordion multiple>
{categories && Object.entries(categories).map(([category, path], index) => (
<AccordionItem key={index} value={category}>
<AccordionHeader><Label size={"large"}>{category}</Label></AccordionHeader>
<AccordionPanel>
{Object.entries(path).map(([_url, { pathUrl, path }]) => (
Object.entries(path).map(([method, details]) => (
<Tab key={`${pathUrl}-${method}`} value={`${pathUrl}-${method}`}>
<div className="d-flex">
<div
className="fs-6 me-2"
style={{ color: methodColors[method] }}>{method.toUpperCase()}</div>
<div>{details.operationId.replace("WebPubSub_", "")}</div>
</div>
</Tab>
))
))}
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</TabList>
</div>)
}
50 changes: 50 additions & 0 deletions tools/awps-tunnel/client/src/components/api/Methods.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Label } from "@fluentui/react-components";
import 'bootstrap/dist/css/bootstrap.min.css';
import React, { useEffect, useState } from "react";
import { APIResponse, Example, Operation } from "../../models";
import { Parameters } from "./Parameters";
import { Response } from "./Response";

export const methodColors: { [method: string]: string } = {
post: "#ffd02b",
put: "#4385d9",
delete: "#f27263",
head: "#a887c9"
};

export function Method({ method, path, methodName }: {
method: Operation,
path: string,
methodName: string
}): React.JSX.Element {
const [example, setExample] = useState<Example>();
const [response, setResponse] = useState<APIResponse | undefined>(undefined);
useEffect(() => {
if (method.operationId) {
const operationId = method.operationId;
const example = method["x-ms-examples"][operationId].$ref;
fetch(`./api/${process.env.REACT_APP_API_VERSION}/${example}`).then(res => res.json()).then(res => setExample(res))
setResponse(undefined);
}
}, [method, path]);

return (
<div className="d-flex">
<div className="p-3">
<Label className="fs-4 fw-bold ms-2">{method.summary}</Label>

<div className="d-flex flex-row justify-content-start align-items-center ms-2">
<div className={"g-primary text-white rounded px-1 fs-6"} style={{ backgroundColor: methodColors[methodName] }}>{methodName.toUpperCase()}
</div>
<div className="mx-2">{path}</div>
</div>


{method.parameters && example &&
<Parameters path={path} parameters={method.parameters} example={example.parameters}
setResponse={setResponse} methodName={methodName} />}
<Response responseSchema={method.responses} response={response} />
</div>
</div>
)
}
Loading

0 comments on commit f209560

Please sign in to comment.