Skip to content

Commit 73991b3

Browse files
committed
1 parent f4a4569 commit 73991b3

19 files changed

+1590
-11
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"recharts": "^3.2.1",
6161
"reodotdev": "^1.0.0",
6262
"sonner": "^2.0.1",
63+
"swagger-ui-react": "^5.29.0",
6364
"tailwind-merge": "^3.0.2",
6465
"tailwindcss": "^4.0.9",
6566
"tailwindcss-animate": "^1.0.7",
@@ -76,6 +77,7 @@
7677
"@testing-library/react": "^16.3.0",
7778
"@types/react": "^19.0.8",
7879
"@types/react-dom": "^19.0.3",
80+
"@types/swagger-ui-react": "^5.18.0",
7981
"@vitejs/plugin-react": "^5.0.0",
8082
"@vitest/coverage-v8": "^3.2.4",
8183
"dotenv-cli": "^10.0.0",

pnpm-lock.yaml

Lines changed: 1220 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Breadcrumbs.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export function Breadcrumbs() {
4343
} else if (name.startsWith('Ins ')) {
4444
// id = route.split('ins-').pop();
4545
name = instance?.name?.split('.')?.shift() || 'Instance';
46+
} else if (name === 'Apis') {
47+
name = 'APIs';
4648
}
4749

4850
breadcrumbs.push(

src/config/getInstanceClient.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,24 @@ import axios from 'axios';
55
interface InstanceClient {
66
id?: EntityIds;
77
operationsUrl?: string | null;
8+
port?: number;
9+
secure?: boolean;
810
}
911

10-
export function getInstanceClient({ id = OverallAppSignIn, operationsUrl }: InstanceClient = {}) {
11-
const baseURL = operationsUrl || authStore.getOperationsUrl(id);
12+
export function getInstanceClient({ id = OverallAppSignIn, operationsUrl, port, secure }: InstanceClient = {}) {
13+
let baseURL = operationsUrl || authStore.getOperationsUrl(id);
14+
if (baseURL) {
15+
if (port || secure !== undefined) {
16+
const newURL = new URL(baseURL);
17+
if (port) {
18+
newURL.port = String(port);
19+
}
20+
if (secure !== undefined) {
21+
newURL.protocol = secure ? 'https:' : 'http:';
22+
}
23+
baseURL = newURL.toString();
24+
}
25+
}
1226
const client = axios.create({
1327
withCredentials: true,
1428
timeout: 15000,

src/config/useInstanceClient.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,29 @@ import { InstanceClientConfig, InstanceClientIdConfig, InstanceTypeConfig } from
44
import { OverallAppSignIn } from '@/lib/authStore';
55
import { useParams } from '@tanstack/react-router';
66

7-
export function useInstanceClient(operationsUrl?: string | null) {
7+
export function useInstanceClient(operationsUrl?: string | null, port?: number, secure?: boolean) {
88
const { instanceId, clusterId }: { instanceId?: string; clusterId?: string; } = useParams({ strict: false });
99
const id = isLocalStudio ? OverallAppSignIn : instanceId ?? clusterId;
10-
return getInstanceClient({ id, operationsUrl });
10+
return getInstanceClient({ id, operationsUrl, port, secure });
1111
}
1212

13-
export function useInstanceClientParams(operationsUrl?: string | null): InstanceClientConfig & InstanceTypeConfig {
13+
export function useInstanceClientParams(operationsUrl?: string | null, port?: number, secure?: boolean): InstanceClientConfig & InstanceTypeConfig {
1414
const { instanceId, clusterId }: { instanceId?: string; clusterId?: string; } = useParams({ strict: false });
1515
const id = isLocalStudio ? OverallAppSignIn : instanceId ?? clusterId;
1616
return {
17-
instanceClient: getInstanceClient({ id, operationsUrl }),
17+
instanceClient: getInstanceClient({ id, operationsUrl, port, secure }),
1818
entityType: (isLocalStudio || instanceId) ? 'instance' : 'cluster',
1919
};
2020
}
2121

22-
export function useInstanceClientIdParams(operationsUrl?: string | null): InstanceClientIdConfig & InstanceTypeConfig {
22+
export function useInstanceClientIdParams(operationsUrl?: string | null, port?: number, secure?: boolean): InstanceClientIdConfig & InstanceTypeConfig {
2323
const { instanceId, clusterId }: { instanceId?: string; clusterId?: string; } = useParams({ strict: false });
2424
const id = isLocalStudio ? OverallAppSignIn : instanceId ?? clusterId;
2525
if (!id) {
2626
throw new Error('id could not be automatically calculated in useInstanceClientIdParams');
2727
}
2828
return {
29-
instanceClient: getInstanceClient({ id, operationsUrl }),
29+
instanceClient: getInstanceClient({ id, operationsUrl, port, secure }),
3030
entityId: id,
3131
entityType: (isLocalStudio || instanceId) ? 'instance' : 'cluster',
3232
};

src/features/instance/InstanceNavBar.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useInstanceManagePermission } from '@/hooks/usePermissions';
1212
import { excludeFalsy } from '@/lib/arrays/excludeFalsy';
1313
import { buildAbsoluteLinkToPage } from '@/lib/urls/buildAbsoluteLinkToPage';
1414
import { Link, useParams } from '@tanstack/react-router';
15-
import { DatabaseIcon, GaugeIcon, Menu, NotepadTextIcon, PackageIcon, SettingsIcon } from 'lucide-react';
15+
import { DatabaseIcon, GaugeIcon, Menu, NotepadTextIcon, PackageIcon, ServerIcon, SettingsIcon } from 'lucide-react';
1616
import { ReactNode, useMemo } from 'react';
1717

1818
interface Link {
@@ -32,6 +32,11 @@ export function InstanceNavBar() {
3232
shortName: 'Apps',
3333
icon: <PackageIcon className="inline-block" />,
3434
},
35+
canManage && {
36+
to: buildAbsoluteLinkToPage(params, 'apis'),
37+
name: 'APIs',
38+
icon: <ServerIcon className="inline-block" />,
39+
},
3540
{
3641
to: buildAbsoluteLinkToPage(params, 'databases'),
3742
icon: <DatabaseIcon className="inline-block" />,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ErrorComponent } from '@/components/ErrorComponent';
2+
import { Loading } from '@/components/Loading';
3+
import { useInstanceClientIdParams } from '@/config/useInstanceClient';
4+
import { plugins } from '@/features/instance/apis/plugins';
5+
import { requestSnippets } from '@/features/instance/apis/requestSnippets';
6+
import { getConfigurationQueryOptions } from '@/features/instance/operations/queries/getConfiguration';
7+
import { getOpenAPIQueryOptions } from '@/features/instance/operations/queries/getOpenAPI';
8+
import { getRegistrationInfoQueryOptions } from '@/features/instance/operations/queries/getRegistrationInfo';
9+
import { wasAReleasedBeforeB } from '@/lib/string/wasAReleasedBeforeB';
10+
import { useQuery } from '@tanstack/react-query';
11+
import { useParams } from '@tanstack/react-router';
12+
import SwaggerUI from 'swagger-ui-react';
13+
import 'swagger-ui-react/swagger-ui.css';
14+
import './swagger.css';
15+
16+
export function APIDocs() {
17+
const { instanceId, clusterId }: { instanceId?: string; clusterId?: string; } = useParams({ strict: false });
18+
const operationsParams = useInstanceClientIdParams();
19+
const {
20+
data: configurationInfo,
21+
isLoading: isLoadingConfiguration,
22+
} = useQuery(getConfigurationQueryOptions(operationsParams));
23+
const { data: registrationInfo, isLoading: isLoadingRegistration } = useQuery(
24+
getRegistrationInfoQueryOptions(operationsParams),
25+
);
26+
const {
27+
data: spec,
28+
isLoading: isLoadingDocs,
29+
error,
30+
} = useQuery(getOpenAPIQueryOptions(operationsParams));
31+
32+
if (isLoadingConfiguration || isLoadingRegistration || isLoadingDocs) {
33+
return <Loading centered={true} text="Looking up your instance configuration, one moment." />;
34+
}
35+
36+
if (error) {
37+
if (registrationInfo?.version && !wasAReleasedBeforeB('4.7.0-beta.7', registrationInfo?.version)) {
38+
return <ErrorComponent
39+
title="API Docs Unavailable"
40+
error={{
41+
message: `API Docs are only available starting in version '4.7.0-beta.7' of Harper, please update your version ${registrationInfo.version}!`,
42+
}}
43+
showReturnToHome={false}
44+
/>;
45+
}
46+
47+
return <ErrorComponent
48+
title="API Docs Unavailable"
49+
error={{
50+
message: 'We weren\'t able to look up your docs. Please check the Network tab of your' +
51+
' developer tools to see why the docs were not accessible to Studio.',
52+
}}
53+
showReturnToHome={false}
54+
/>;
55+
}
56+
57+
return (<>
58+
{configurationInfo?.http?.cors === false && (<ErrorComponent
59+
title="CORS Disabled: API Not Accessible"
60+
className="mt-0 mx-4 m-0"
61+
error={{
62+
message: `This ${clusterId && !instanceId ? 'cluster' : 'instance'} has CORS disabled currently, so you won't be able to execute API requests from the browser.`,
63+
}}
64+
showReturnToHome={false}
65+
/>)}
66+
<SwaggerUI
67+
spec={spec}
68+
persistAuthorization={true}
69+
requestSnippetsEnabled={true}
70+
requestSnippets={requestSnippets}
71+
plugins={plugins}
72+
tryItOutEnabled={true}
73+
/>
74+
</>);
75+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { APIDocs } from '@/features/instance/apis/APIDocs';
2+
import { createLazyRoute } from '@tanstack/react-router';
3+
4+
export const route = createLazyRoute('/apis')({
5+
component: APIDocs,
6+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export const SnippedGeneratorNodeJsPlugin = {
2+
fn: {
3+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4+
requestSnippetGenerator_node_fetch: (request: any) => {
5+
const url = new URL(request.get('url'));
6+
let isMultipartFormDataRequest = false;
7+
const headers = request.get('headers');
8+
if (headers && headers.size) {
9+
request.get('headers').map((val: string, key: string) => {
10+
isMultipartFormDataRequest = isMultipartFormDataRequest || /^content-type$/i.test(key) && /^multipart\/form-data$/i.test(val);
11+
});
12+
}
13+
let reqBody = request.get('body');
14+
if (request.get('body')) {
15+
if (isMultipartFormDataRequest && ['POST', 'PUT', 'PATCH'].includes(request.get('method'))) {
16+
return 'throw new Error("Currently unsupported content-type: /^multipart\\/form-data$/i");';
17+
} else {
18+
if (typeof reqBody !== 'string') {
19+
reqBody = JSON.stringify(reqBody, null, '\t');
20+
}
21+
}
22+
} else if (!request.get('body') && request.get('method') === 'POST') {
23+
reqBody = '';
24+
}
25+
26+
const stringBody = '`' + (reqBody || '')
27+
.replace(/\\n/g, '\n')
28+
.replace(/`/g, '\\`')
29+
+ '`';
30+
31+
return `async function main() {
32+
\tconst response = await fetch("${url.toString()}", {
33+
\t\tmethod: "${request.get('method')}",${headers && headers.size ? `
34+
\t\theaders: {
35+
\t\t\t${request.get('headers').map((val: string, key: string) => `"${key}": "${val}"`).valueSeq().join(',\n\t\t\t')}
36+
\t\t},` : ''}${reqBody ? `
37+
\t\tbody: JSON.stringify(${stringBody}),` : ''}
38+
\t});
39+
\tconst data = await response.json();
40+
\tconsole.log(data);
41+
}
42+
43+
main().catch(console.error);
44+
`;
45+
},
46+
},
47+
};
48+
49+
50+
export const plugins = [
51+
SnippedGeneratorNodeJsPlugin,
52+
];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const requestSnippets = {
2+
generators: {
3+
node_fetch: {
4+
title: 'Node.js Fetch',
5+
syntax: 'javascript',
6+
},
7+
},
8+
};

0 commit comments

Comments
 (0)