Skip to content

Commit 22b592a

Browse files
authored
FUI - Operation console (#2655)
1 parent a880c74 commit 22b592a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4627
-74
lines changed
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import * as React from "react";
2+
import { useCallback, useEffect, useRef, useState } from "react";
3+
import { ISettingsProvider } from "@paperbits/common/configuration";
4+
import { SessionManager } from "@paperbits/common/persistence/sessionManager";
5+
import { HttpClient } from "@paperbits/common/http/httpClient";
6+
import {
7+
Body1,
8+
Body1Strong,
9+
Breadcrumb,
10+
BreadcrumbButton,
11+
BreadcrumbDivider,
12+
BreadcrumbItem,
13+
Button,
14+
DrawerBody,
15+
DrawerHeader,
16+
DrawerHeaderTitle,
17+
OverlayDrawer,
18+
Spinner,
19+
Tooltip,
20+
isTruncatableBreadcrumbContent,
21+
truncateBreadcrumbLongName
22+
} from "@fluentui/react-components";
23+
import { DismissRegular } from "@fluentui/react-icons";
24+
import { RouteHelper } from "../../../../../routing/routeHelper";
25+
import { Api } from "../../../../../models/api";
26+
import { Operation } from "../../../../../models/operation";
27+
import { ConsoleOperation } from "../../../../../models/console/consoleOperation";
28+
import { ConsoleHeader } from "../../../../../models/console/consoleHeader";
29+
import { ConsoleParameter } from "../../../../../models/console/consoleParameter";
30+
import { AuthorizationServer } from "../../../../../models/authorizationServer";
31+
import { KnownHttpHeaders } from "../../../../../models/knownHttpHeaders";
32+
import { ApiService } from "../../../../../services/apiService";
33+
import { OAuthService } from "../../../../../services/oauthService";
34+
import { ProductService } from "../../../../../services/productService";
35+
import { TenantService } from "../../../../../services/tenantService";
36+
import { UsersService } from "../../../../../services/usersService";
37+
import { ServiceSkuName, TypeOfApi } from "../../../../../constants";
38+
import { ConsoleAuthorization } from "./operation-console/ConsoleAuthorization";
39+
import { ConsoleBody } from "./operation-console/ConsoleBody";
40+
import { ConsoleHeaders } from "./operation-console/ConsoleHeaders";
41+
import { ConsoleHosts } from "./operation-console/ConsoleHosts";
42+
import { ConsoleParameters } from "./operation-console/ConsoleParameters";
43+
import { ConsoleRequestResponse } from "./operation-console/ConsoleRequestResponse";
44+
import { getAuthServers, getBackendUrl, loadSubscriptionKeys, setupOAuth } from "./operation-console/consoleUtils";
45+
46+
type OperationConsoleProps = {
47+
isOpen: boolean;
48+
setIsOpen: (isOpen: boolean) => void;
49+
api: Api;
50+
operation: Operation;
51+
hostnames: string[];
52+
useCorsProxy: boolean;
53+
apiService: ApiService;
54+
usersService: UsersService;
55+
productService: ProductService;
56+
oauthService: OAuthService;
57+
tenantService: TenantService;
58+
routeHelper: RouteHelper;
59+
settingsProvider: ISettingsProvider;
60+
sessionManager: SessionManager;
61+
httpClient: HttpClient;
62+
}
63+
64+
interface StoredCredentials {
65+
grantType: string;
66+
accessToken: string;
67+
}
68+
69+
interface OAuthSession {
70+
[apiName: string]: StoredCredentials;
71+
}
72+
73+
export const OperationConsole = ({
74+
isOpen,
75+
setIsOpen,
76+
api,
77+
operation,
78+
hostnames,
79+
useCorsProxy,
80+
apiService,
81+
usersService,
82+
productService,
83+
oauthService,
84+
tenantService,
85+
routeHelper,
86+
settingsProvider,
87+
sessionManager,
88+
httpClient
89+
}: OperationConsoleProps) => {
90+
const [working, setWorking] = useState<boolean>(false);
91+
const [authorizationServers, setAuthorizationServers] = useState<AuthorizationServer[]>([]);
92+
const [products, setProducts] = useState<any[]>([]);
93+
const [selectedSubscriptionKey, setSelectedSubscriptionKey] = useState<string>(null);
94+
const [isConsumptionMode, setIsConsumptionMode] = useState<boolean>(false);
95+
const [backendUrl, setBackendUrl] = useState<string>("");
96+
97+
const consoleOperation = useRef(new ConsoleOperation(api, operation));
98+
const [forceRerender, setForceRerender] = useState<number>(1);
99+
const rerender = useCallback(() => setForceRerender(old => old + 1), []);
100+
101+
useEffect(() => {
102+
setWorking(true);
103+
consoleOperation.current.host.hostname(hostnames[0]);
104+
Promise.all([
105+
getAuthServers(api, oauthService).then(authServers => {
106+
setAuthorizationServers(authServers);
107+
if (authServers.length > 0) {
108+
setupOAuth(api, authServers[0], consoleOperation.current.request.headers(), sessionManager).then(newHeaders => {
109+
consoleOperation.current.request.headers(newHeaders);
110+
});
111+
}
112+
}),
113+
loadSubscriptionKeys(api, apiService, productService, usersService).then(products => {
114+
setProducts(products);
115+
if (products.length > 0) {
116+
setSelectedSubscriptionKey(products[0].subscriptionKeys[0]?.value);
117+
}
118+
}),
119+
getSkuName().then(skuName => setIsConsumptionMode(skuName === ServiceSkuName.Consumption)),
120+
getBackendUrl(settingsProvider).then(url => setBackendUrl(url))
121+
])
122+
.catch(error => new Error(`Unable to load the console details. Error: ${error.message}`))
123+
.finally(() => {
124+
setWorking(false);
125+
});
126+
}, [api, operation, consoleOperation]);
127+
128+
useEffect(() => {
129+
if (!isConsumptionMode && api.type !== TypeOfApi.webSocket) {
130+
if (!consoleOperation.current.request.headers().some(header => header.name() === KnownHttpHeaders.CacheControl)) {
131+
consoleOperation.current.setHeader(KnownHttpHeaders.CacheControl, "no-cache", "string", "Disable caching.");
132+
}
133+
}
134+
rerender();
135+
}, [api, isConsumptionMode, consoleOperation, rerender]);
136+
137+
useEffect(() => {
138+
api.subscriptionRequired && setSubscriptionKeyHeader(selectedSubscriptionKey || "");
139+
}, [selectedSubscriptionKey]);
140+
141+
const getSkuName = async (): Promise<string> => {
142+
return await tenantService.getServiceSkuName();
143+
}
144+
145+
const setSubscriptionHeader = (key?: string): ConsoleHeader[] => {
146+
const headers = consoleOperation.current.request.headers();
147+
let subscriptionHeaderName: string = KnownHttpHeaders.OcpApimSubscriptionKey;
148+
149+
if (api.subscriptionKeyParameterNames && api.subscriptionKeyParameterNames.header) {
150+
subscriptionHeaderName = api.subscriptionKeyParameterNames.header;
151+
}
152+
153+
const newHeaders = headers.filter(header => header.name() !== subscriptionHeaderName);
154+
155+
const subscriptionHeader = new ConsoleHeader();
156+
subscriptionHeader.name(subscriptionHeaderName);
157+
subscriptionHeader.value(key || "");
158+
subscriptionHeader.description = "Subscription key.";
159+
subscriptionHeader.required = true;
160+
subscriptionHeader.secret(true);
161+
subscriptionHeader.type = "string";
162+
subscriptionHeader.inputTypeValue("password");
163+
164+
newHeaders.push(subscriptionHeader);
165+
166+
return newHeaders;
167+
}
168+
169+
const setSubscriptionKeyHeader = (key: string = ""): void => {
170+
const newHeaders = setSubscriptionHeader(key);
171+
consoleOperation.current.request.headers(newHeaders);
172+
rerender();
173+
}
174+
175+
const updateHostname = (hostname: string) => {
176+
consoleOperation.current.host.hostname(hostname);
177+
rerender();
178+
}
179+
180+
const updateParameters = (queryParameters: ConsoleParameter[], templateParameters: ConsoleParameter[]) => {
181+
consoleOperation.current.templateParameters(templateParameters);
182+
consoleOperation.current.request.queryParameters(queryParameters);
183+
rerender();
184+
}
185+
186+
const updateHeaders = (headers: ConsoleHeader[]) => {
187+
consoleOperation.current.request.headers(headers);
188+
rerender();
189+
}
190+
191+
const updateBody = (body: string) => {
192+
consoleOperation.current.request.body(body);
193+
rerender();
194+
}
195+
196+
const updateBodyBinary = (body: File) => {
197+
consoleOperation.current.request.binary(body);
198+
rerender();
199+
}
200+
201+
const getApiReferenceUrl = (): string => {
202+
return routeHelper.getApiReferenceUrl(api.name);
203+
}
204+
205+
const renderBreadcrumbItem = (name: string, isCurrent: boolean, url?: string) => (
206+
<>
207+
{isTruncatableBreadcrumbContent(name, 20)
208+
? <BreadcrumbItem>
209+
<Tooltip withArrow content={name} relationship="label">
210+
<BreadcrumbButton href={url ?? ""} current={isCurrent}>{truncateBreadcrumbLongName(name, 20)}</BreadcrumbButton>
211+
</Tooltip>
212+
</BreadcrumbItem>
213+
: <BreadcrumbItem>
214+
<BreadcrumbButton href={url ?? ""} current={isCurrent}>{name}</BreadcrumbButton>
215+
</BreadcrumbItem>
216+
}
217+
</>
218+
)
219+
220+
return (
221+
<OverlayDrawer
222+
open={isOpen}
223+
onOpenChange={(_, { open }) => setIsOpen(open)}
224+
position="end"
225+
size="medium"
226+
className="console-drawer"
227+
>
228+
<DrawerHeader>
229+
<DrawerHeaderTitle
230+
action={
231+
<Button
232+
aria-label="Close console"
233+
appearance="subtle"
234+
icon={<DismissRegular />}
235+
onClick={() => setIsOpen(false)}
236+
/>
237+
}
238+
>
239+
<Breadcrumb>
240+
{renderBreadcrumbItem(api.displayName, false, getApiReferenceUrl())}
241+
{api.apiVersion &&
242+
<>
243+
<BreadcrumbDivider />
244+
<BreadcrumbItem>
245+
<BreadcrumbButton>{api.apiVersion}</BreadcrumbButton>
246+
</BreadcrumbItem>
247+
</>
248+
}
249+
{api.type !== TypeOfApi.webSocket &&
250+
<>
251+
<BreadcrumbDivider />
252+
{renderBreadcrumbItem(consoleOperation?.current.name ?? "", true)}
253+
</>
254+
}
255+
</Breadcrumb>
256+
</DrawerHeaderTitle>
257+
</DrawerHeader>
258+
<DrawerBody>
259+
{(working || !consoleOperation.current) ? <Spinner label="Loading..." labelPosition="below" size="small" />
260+
: <>
261+
{api.type !== TypeOfApi.webSocket && consoleOperation.current.urlTemplate &&
262+
<div className="console-method">
263+
<Body1Strong className={`operation-method method-${consoleOperation.current.method}`}>
264+
{consoleOperation.current.method}
265+
</Body1Strong>
266+
<Body1 className={`operation-name`}>
267+
{consoleOperation.current.urlTemplate}
268+
</Body1>
269+
</div>
270+
}
271+
{hostnames.length > 1 &&
272+
<ConsoleHosts
273+
hostnames={hostnames}
274+
updateHostname={updateHostname}
275+
/>
276+
}
277+
{(authorizationServers.length > 0 || api.subscriptionRequired) &&
278+
<ConsoleAuthorization
279+
api={api}
280+
headers={consoleOperation.current.request.headers()}
281+
products={products}
282+
subscriptionRequired={api.subscriptionRequired}
283+
subscriptionKey={selectedSubscriptionKey}
284+
authorizationServers={authorizationServers}
285+
sessionManager={sessionManager}
286+
oauthService={oauthService}
287+
updateHeaders={updateHeaders}
288+
selectSubscriptionKey={setSelectedSubscriptionKey}
289+
/>
290+
}
291+
<ConsoleParameters
292+
queryParameters={consoleOperation.current.request.queryParameters()}
293+
templateParameters={consoleOperation.current.templateParameters()}
294+
updateParameters={updateParameters}
295+
/>
296+
<ConsoleHeaders
297+
headers={consoleOperation.current.request.headers()}
298+
updateHeaders={updateHeaders}
299+
/>
300+
{(consoleOperation.current.canHaveBody || consoleOperation.current.hasBody()) &&
301+
<ConsoleBody
302+
hasBody={consoleOperation.current.hasBody()}
303+
request={consoleOperation.current.request}
304+
updateBody={updateBody}
305+
updateBodyBinary={updateBodyBinary}
306+
/>
307+
}
308+
<ConsoleRequestResponse
309+
api={api}
310+
consoleOperation={consoleOperation.current}
311+
backendUrl={backendUrl}
312+
useCorsProxy={useCorsProxy}
313+
httpClient={httpClient}
314+
forceRerender={forceRerender}
315+
/>
316+
</>
317+
}
318+
</DrawerBody>
319+
</OverlayDrawer>
320+
);
321+
}

0 commit comments

Comments
 (0)