Skip to content

Commit 89e2463

Browse files
authored
[codex] Refresh API demo panel with clearer request sections and stronger live testing controls (#105)
* refresh api demo panel * fix api demo panel review feedback * fix api demo panel follow-up review feedback * fix param options review feedback * fix primitive select option normalization
1 parent 258dbd3 commit 89e2463

13 files changed

Lines changed: 1670 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from "react";
2+
3+
import FormItem from "@theme/ApiDemoPanel/FormItem";
4+
import FormSelect from "@theme/ApiDemoPanel/FormSelect";
5+
import FormTextInput from "@theme/ApiDemoPanel/FormTextInput";
6+
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
7+
8+
import sharedStyles from "../shared.module.css";
9+
import {
10+
resolveAuthFieldMeta,
11+
resolveSchemeOptionLabel,
12+
} from "../utils";
13+
import {
14+
setAuthData,
15+
setSelectedAuth,
16+
} from "@theme/ApiDemoPanel/Authorization/slice";
17+
18+
function Authorization() {
19+
const data = useTypedSelector((state: any) => state.auth.data);
20+
const options = useTypedSelector((state: any) => state.auth.options);
21+
const selected = useTypedSelector((state: any) => state.auth.selected);
22+
const dispatch = useTypedDispatch();
23+
24+
if (selected === undefined) {
25+
return null;
26+
}
27+
28+
const selectedAuth = options[selected] || [];
29+
const optionEntries = Object.entries(options);
30+
const handleFieldChange =
31+
(scheme: string, key: string) =>
32+
(event: React.ChangeEvent<HTMLInputElement>) => {
33+
const nextValue = event.target.value;
34+
dispatch(
35+
setAuthData({
36+
scheme,
37+
key,
38+
value: nextValue ? nextValue : undefined,
39+
})
40+
);
41+
};
42+
43+
if (selectedAuth.length === 0) {
44+
return null;
45+
}
46+
47+
return (
48+
<div className={sharedStyles.section}>
49+
<h5 className={sharedStyles.sectionTitle}>Authorization</h5>
50+
<p className={sharedStyles.sectionDescription}>
51+
Provide any credentials required for the live request. Header names stay
52+
visible as helper text.
53+
</p>
54+
55+
{optionEntries.length > 1 && (
56+
<FormItem
57+
description="Choose which credential set should be attached to this request."
58+
label="Security Scheme"
59+
>
60+
<FormSelect
61+
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
62+
dispatch(setSelectedAuth(event.target.value))
63+
}
64+
options={optionEntries.map(([key, schemes]) => ({
65+
label: resolveSchemeOptionLabel(key, schemes as any[]),
66+
value: key,
67+
}))}
68+
value={selected}
69+
/>
70+
</FormItem>
71+
)}
72+
73+
{selectedAuth.flatMap((scheme: any) =>
74+
resolveAuthFieldMeta(scheme).map((field) => (
75+
<FormItem
76+
description={field.description}
77+
helperText={field.helperText}
78+
key={`${scheme.key}-${field.dataKey}`}
79+
label={field.label}
80+
>
81+
<FormTextInput
82+
onChange={handleFieldChange(scheme.key, field.dataKey)}
83+
password={field.password}
84+
placeholder={field.placeholder}
85+
value={data[scheme.key]?.[field.dataKey] ?? ""}
86+
/>
87+
</FormItem>
88+
))
89+
)}
90+
</div>
91+
);
92+
}
93+
94+
export default Authorization;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from "react";
2+
3+
import sdk from "@paloaltonetworks/postman-collection";
4+
import buildPostmanRequest from "@theme/ApiDemoPanel/buildPostmanRequest";
5+
import { Param } from "@theme/ApiDemoPanel/ParamOptions/slice";
6+
import { setResponse } from "@theme/ApiDemoPanel/Response/slice";
7+
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
8+
9+
import sharedStyles from "../shared.module.css";
10+
import makeRequest from "@theme/ApiDemoPanel/Execute/makeRequest";
11+
12+
function validateRequest(params: {
13+
path: Param[];
14+
query: Param[];
15+
header: Param[];
16+
cookie: Param[];
17+
}) {
18+
for (const paramList of Object.values(params)) {
19+
for (const param of paramList) {
20+
if (param.required && !param.value) {
21+
return false;
22+
}
23+
}
24+
}
25+
26+
return true;
27+
}
28+
29+
export interface Props {
30+
postman: sdk.Request;
31+
proxy?: string;
32+
}
33+
34+
function Execute({ postman, proxy }: Props) {
35+
const pathParams = useTypedSelector((state: any) => state.params.path);
36+
const queryParams = useTypedSelector((state: any) => state.params.query);
37+
const cookieParams = useTypedSelector((state: any) => state.params.cookie);
38+
const headerParams = useTypedSelector((state: any) => state.params.header);
39+
const contentType = useTypedSelector((state: any) => state.contentType.value);
40+
const body = useTypedSelector((state: any) => state.body);
41+
const accept = useTypedSelector((state: any) => state.accept.value);
42+
const server = useTypedSelector((state: any) => state.server.value);
43+
const params = useTypedSelector((state: any) => state.params);
44+
const auth = useTypedSelector((state: any) => state.auth);
45+
const dispatch = useTypedDispatch();
46+
47+
const isValidRequest = validateRequest(params);
48+
const postmanRequest = buildPostmanRequest(postman, {
49+
accept,
50+
auth,
51+
body,
52+
contentType,
53+
cookieParams,
54+
headerParams,
55+
pathParams,
56+
queryParams,
57+
server,
58+
});
59+
60+
const delay = (ms: number) =>
61+
new Promise((resolve) => setTimeout(resolve, ms));
62+
const handleActionClickCapture = (event: React.MouseEvent<HTMLDivElement>) => {
63+
if (isValidRequest) {
64+
return;
65+
}
66+
67+
event.preventDefault();
68+
event.stopPropagation();
69+
};
70+
71+
return (
72+
<div
73+
className={sharedStyles.actionStack}
74+
onClickCapture={handleActionClickCapture}
75+
>
76+
<button
77+
className={`button button--primary button--sm ${sharedStyles.actionButton}`}
78+
disabled={!isValidRequest}
79+
onClick={async () => {
80+
dispatch(setResponse("Fetching..."));
81+
82+
try {
83+
await delay(1200);
84+
const response = await makeRequest(postmanRequest, proxy, body);
85+
dispatch(setResponse(response));
86+
} catch {
87+
dispatch(setResponse("Connection failed"));
88+
}
89+
}}
90+
type="button"
91+
>
92+
Send API Request
93+
</button>
94+
{!isValidRequest && (
95+
<p className={sharedStyles.actionHint}>
96+
Complete required inputs to send the request.
97+
</p>
98+
)}
99+
</div>
100+
);
101+
}
102+
103+
export default Execute;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from "react";
2+
3+
import sharedStyles from "../shared.module.css";
4+
import { getLocationLabel } from "../utils";
5+
6+
export interface Props {
7+
label?: React.ReactNode;
8+
type?: string;
9+
required?: boolean;
10+
description?: React.ReactNode;
11+
helperText?: React.ReactNode;
12+
children?: React.ReactNode;
13+
}
14+
15+
function FormItem({
16+
label,
17+
type,
18+
required,
19+
description,
20+
helperText,
21+
children,
22+
}: Props) {
23+
const typeLabel = getLocationLabel(type);
24+
25+
return (
26+
<div className={sharedStyles.field}>
27+
{(label || typeLabel || required) && (
28+
<div className={sharedStyles.fieldHeader}>
29+
{label && <span className={sharedStyles.fieldLabel}>{label}</span>}
30+
{typeLabel && <span className={sharedStyles.fieldType}>{typeLabel}</span>}
31+
{required && (
32+
<span className={sharedStyles.requiredBadge}>Required</span>
33+
)}
34+
</div>
35+
)}
36+
{description && (
37+
<p className={sharedStyles.fieldDescription}>{description}</p>
38+
)}
39+
<div className={sharedStyles.fieldBody}>{children}</div>
40+
{helperText && <p className={sharedStyles.fieldHelp}>{helperText}</p>}
41+
</div>
42+
);
43+
}
44+
45+
export default FormItem;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from "react";
2+
3+
import sharedStyles from "../shared.module.css";
4+
import { normalizeSelectOptions, SelectOptionInput } from "../utils";
5+
6+
export interface Props {
7+
value?: string;
8+
options?: SelectOptionInput[];
9+
onChange?: React.ChangeEventHandler<HTMLSelectElement>;
10+
}
11+
12+
function FormSelect({ value, options, onChange }: Props) {
13+
const normalizedOptions = normalizeSelectOptions(options);
14+
15+
if (normalizedOptions.length === 0) {
16+
return null;
17+
}
18+
19+
return (
20+
<select
21+
className={sharedStyles.selectControl}
22+
onChange={onChange}
23+
value={value}
24+
>
25+
{normalizedOptions.map((option, index) => (
26+
<option key={`${option.value}-${index}`} value={option.value}>
27+
{option.label}
28+
</option>
29+
))}
30+
</select>
31+
);
32+
}
33+
34+
export default FormSelect;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from "react";
2+
3+
import sharedStyles from "../shared.module.css";
4+
5+
export interface Props {
6+
value?: string;
7+
placeholder?: string;
8+
password?: boolean;
9+
readOnly?: boolean;
10+
onChange?: React.ChangeEventHandler<HTMLInputElement>;
11+
}
12+
13+
function FormTextInput({
14+
value,
15+
placeholder,
16+
password,
17+
readOnly,
18+
onChange,
19+
}: Props) {
20+
const sanitizedPlaceholder = placeholder?.split("\n")[0];
21+
22+
return (
23+
<input
24+
autoComplete={password ? "new-password" : "off"}
25+
className={sharedStyles.control}
26+
onChange={onChange}
27+
placeholder={sanitizedPlaceholder}
28+
readOnly={readOnly}
29+
spellCheck={false}
30+
title={sanitizedPlaceholder}
31+
type={password ? "password" : "text"}
32+
value={value}
33+
/>
34+
);
35+
}
36+
37+
export default FormTextInput;

0 commit comments

Comments
 (0)