Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/theme/ApiDemoPanel/Authorization/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from "react";

import FormItem from "@theme/ApiDemoPanel/FormItem";
import FormSelect from "@theme/ApiDemoPanel/FormSelect";
import FormTextInput from "@theme/ApiDemoPanel/FormTextInput";
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";

import sharedStyles from "../shared.module.css";
import {
resolveAuthFieldMeta,
resolveSchemeOptionLabel,
} from "../utils";
import {
setAuthData,
setSelectedAuth,
} from "@theme/ApiDemoPanel/Authorization/slice";

function Authorization() {
const data = useTypedSelector((state: any) => state.auth.data);
const options = useTypedSelector((state: any) => state.auth.options);
const selected = useTypedSelector((state: any) => state.auth.selected);
const dispatch = useTypedDispatch();

if (selected === undefined) {
return null;
}

const selectedAuth = options[selected] || [];
const optionEntries = Object.entries(options);
const handleFieldChange =
(scheme: string, key: string) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
const nextValue = event.target.value;
dispatch(
setAuthData({
scheme,
key,
value: nextValue ? nextValue : undefined,
})
);
};

if (selectedAuth.length === 0) {
return null;
}

return (
<div className={sharedStyles.section}>
<h5 className={sharedStyles.sectionTitle}>Authorization</h5>
<p className={sharedStyles.sectionDescription}>
Provide any credentials required for the live request. Header names stay
visible as helper text.
</p>

{optionEntries.length > 1 && (
<FormItem
description="Choose which credential set should be attached to this request."
label="Security Scheme"
>
<FormSelect
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
dispatch(setSelectedAuth(event.target.value))
}
options={optionEntries.map(([key, schemes]) => ({
label: resolveSchemeOptionLabel(key, schemes as any[]),
value: key,
}))}
value={selected}
/>
</FormItem>
)}

{selectedAuth.flatMap((scheme: any) =>
resolveAuthFieldMeta(scheme).map((field) => (
<FormItem
description={field.description}
helperText={field.helperText}
key={`${scheme.key}-${field.dataKey}`}
label={field.label}
>
<FormTextInput
onChange={handleFieldChange(scheme.key, field.dataKey)}
password={field.password}
placeholder={field.placeholder}
value={data[scheme.key]?.[field.dataKey] ?? ""}
/>
</FormItem>
))
)}
</div>
);
}

export default Authorization;
103 changes: 103 additions & 0 deletions src/theme/ApiDemoPanel/Execute/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from "react";

import sdk from "@paloaltonetworks/postman-collection";
import buildPostmanRequest from "@theme/ApiDemoPanel/buildPostmanRequest";
import { Param } from "@theme/ApiDemoPanel/ParamOptions/slice";
import { setResponse } from "@theme/ApiDemoPanel/Response/slice";
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";

import sharedStyles from "../shared.module.css";
import makeRequest from "@theme/ApiDemoPanel/Execute/makeRequest";

function validateRequest(params: {
path: Param[];
query: Param[];
header: Param[];
cookie: Param[];
}) {
for (const paramList of Object.values(params)) {
for (const param of paramList) {
if (param.required && !param.value) {
return false;
}
}
}

return true;
}

export interface Props {
postman: sdk.Request;
proxy?: string;
}

function Execute({ postman, proxy }: Props) {
const pathParams = useTypedSelector((state: any) => state.params.path);
const queryParams = useTypedSelector((state: any) => state.params.query);
const cookieParams = useTypedSelector((state: any) => state.params.cookie);
const headerParams = useTypedSelector((state: any) => state.params.header);
const contentType = useTypedSelector((state: any) => state.contentType.value);
const body = useTypedSelector((state: any) => state.body);
const accept = useTypedSelector((state: any) => state.accept.value);
const server = useTypedSelector((state: any) => state.server.value);
const params = useTypedSelector((state: any) => state.params);
const auth = useTypedSelector((state: any) => state.auth);
const dispatch = useTypedDispatch();

const isValidRequest = validateRequest(params);
const postmanRequest = buildPostmanRequest(postman, {
accept,
auth,
body,
contentType,
cookieParams,
headerParams,
pathParams,
queryParams,
server,
});

const delay = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
const handleActionClickCapture = (event: React.MouseEvent<HTMLDivElement>) => {
if (isValidRequest) {
return;
}

event.preventDefault();
event.stopPropagation();
};

return (
<div
className={sharedStyles.actionStack}
onClickCapture={handleActionClickCapture}
>
<button
className={`button button--primary button--sm ${sharedStyles.actionButton}`}
disabled={!isValidRequest}
onClick={async () => {
Comment thread
basit3407 marked this conversation as resolved.
dispatch(setResponse("Fetching..."));

try {
await delay(1200);
const response = await makeRequest(postmanRequest, proxy, body);
dispatch(setResponse(response));
} catch {
dispatch(setResponse("Connection failed"));
}
}}
type="button"
>
Send API Request
</button>
{!isValidRequest && (
<p className={sharedStyles.actionHint}>
Complete required inputs to send the request.
</p>
)}
</div>
);
}

export default Execute;
45 changes: 45 additions & 0 deletions src/theme/ApiDemoPanel/FormItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from "react";

import sharedStyles from "../shared.module.css";
import { getLocationLabel } from "../utils";

export interface Props {
label?: React.ReactNode;
type?: string;
required?: boolean;
description?: React.ReactNode;
helperText?: React.ReactNode;
children?: React.ReactNode;
}

function FormItem({
label,
type,
required,
description,
helperText,
children,
}: Props) {
const typeLabel = getLocationLabel(type);

return (
<div className={sharedStyles.field}>
{(label || typeLabel || required) && (
<div className={sharedStyles.fieldHeader}>
{label && <span className={sharedStyles.fieldLabel}>{label}</span>}
{typeLabel && <span className={sharedStyles.fieldType}>{typeLabel}</span>}
{required && (
<span className={sharedStyles.requiredBadge}>Required</span>
)}
</div>
)}
{description && (
<p className={sharedStyles.fieldDescription}>{description}</p>
)}
<div className={sharedStyles.fieldBody}>{children}</div>
{helperText && <p className={sharedStyles.fieldHelp}>{helperText}</p>}
</div>
);
}

export default FormItem;
34 changes: 34 additions & 0 deletions src/theme/ApiDemoPanel/FormSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";

import sharedStyles from "../shared.module.css";
import { normalizeSelectOptions, SelectOption } from "../utils";

export interface Props {
value?: string;
options?: Array<string | SelectOption>;
onChange?: React.ChangeEventHandler<HTMLSelectElement>;
}

function FormSelect({ value, options, onChange }: Props) {
const normalizedOptions = normalizeSelectOptions(options);

if (normalizedOptions.length === 0) {
return null;
}

return (
<select
className={sharedStyles.selectControl}
onChange={onChange}
value={value}
>
{normalizedOptions.map((option, index) => (
<option key={`${option.value}-${index}`} value={option.value}>
{option.label}
</option>
))}
</select>
);
}

export default FormSelect;
37 changes: 37 additions & 0 deletions src/theme/ApiDemoPanel/FormTextInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";

import sharedStyles from "../shared.module.css";

export interface Props {
value?: string;
placeholder?: string;
password?: boolean;
readOnly?: boolean;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}

function FormTextInput({
value,
placeholder,
password,
readOnly,
onChange,
}: Props) {
const sanitizedPlaceholder = placeholder?.split("\n")[0];

return (
<input
autoComplete={password ? "new-password" : "off"}
className={sharedStyles.control}
onChange={onChange}
placeholder={sanitizedPlaceholder}
readOnly={readOnly}
spellCheck={false}
title={sanitizedPlaceholder}
type={password ? "password" : "text"}
value={value}
/>
);
}

export default FormTextInput;
Loading
Loading