/* ============================================================================ * Copyright (c) Palo Alto Networks * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * ========================================================================== */ import React, { useState, useEffect } from "react"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import ApiCodeBlock from "@theme/ApiExplorer/ApiCodeBlock"; import buildPostmanRequest from "@theme/ApiExplorer/buildPostmanRequest"; import CodeTabs from "@theme/ApiExplorer/CodeTabs"; import { useResolvedEncoding } from "@theme/ApiExplorer/EncodingSelection/useResolvedEncoding"; import { useTypedSelector } from "@theme/ApiItem/hooks"; import cloneDeep from "lodash/cloneDeep"; import codegen from "postman-code-generators"; import * as sdk from "postman-collection"; import { CodeSample, Language } from "./code-snippets-types"; import { getCodeSampleSourceFromLanguage, mergeArraysbyLanguage, mergeCodeSampleLanguage, generateLanguageSet, } from "./languages"; export const languageSet: Language[] = generateLanguageSet(); export interface Props { postman: sdk.Request; codeSamples: CodeSample[]; maskCredentials?: boolean; requestBody?: import("docusaurus-plugin-openapi-docs/src/openapi/types").RequestBodyObject; } function CodeTab({ children, hidden, className }: any): React.JSX.Element { return ( ); } function CodeSnippets({ postman, codeSamples, maskCredentials: propMaskCredentials, requestBody, }: Props) { const { siteConfig } = useDocusaurusContext(); const contentType = useTypedSelector((state: any) => state.contentType.value); const accept = useTypedSelector((state: any) => state.accept.value); const server = useTypedSelector((state: any) => state.server.value); const body = useTypedSelector((state: any) => state.body); 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 auth = useTypedSelector((state: any) => state.auth); // Check if credential masking is enabled (default: true) const maskCredentials = propMaskCredentials ?? true; // Clone Auth if maskCredentials is not false const cleanedAuth = maskCredentials ? (() => { const clonedAuth = cloneDeep(auth); let placeholder: string; function cleanCredentials(obj: any) { for (const key in obj) { if (typeof obj[key] === "object" && obj[key] !== null) { // use name as placeholder if exists const comboAuthId = Object.keys(obj).join(" and "); const authOptions = clonedAuth?.options?.[key] ?? clonedAuth?.options?.[comboAuthId]; placeholder = authOptions?.find( (opt: any) => opt.key === key )?.name; obj[key] = cleanCredentials(obj[key]); } else { obj[key] = `<${placeholder ?? key}>`; } } return obj; } return { ...clonedAuth, data: cleanCredentials(clonedAuth.data), }; })() : auth; const encoding = useResolvedEncoding(requestBody); // Create a Postman request object using cleanedAuth or original auth const cleanedPostmanRequest = buildPostmanRequest(postman, { queryParams, pathParams, cookieParams, contentType, accept, headerParams, body, server, auth: cleanedAuth, encoding, }); // User-defined languages array // Can override languageSet, change order of langs, override options and variants const userDefinedLanguageSet = (siteConfig?.themeConfig?.languageTabs as Language[] | undefined) ?? languageSet; // Filter languageSet by user-defined langs const filteredLanguageSet = languageSet.filter((ls) => { return userDefinedLanguageSet?.some((lang) => { return lang.language === ls.language; }); }); // Merge user-defined langs into languageSet const mergedLangs = mergeCodeSampleLanguage( mergeArraysbyLanguage(userDefinedLanguageSet, filteredLanguageSet), codeSamples ); // Read defaultLang from localStorage const defaultLang: Language[] = mergedLangs.filter( (lang) => lang.language === localStorage.getItem("docusaurus.tab.code-samples") ); const [selectedVariant, setSelectedVariant] = useState(); const [selectedSample, setSelectedSample] = useState(); const [language, setLanguage] = useState(() => { // Return first index if only 1 user-defined language exists if (mergedLangs.length === 1) { return mergedLangs[0]; } // Fall back to language in localStorage or first user-defined language return defaultLang[0] ?? mergedLangs[0]; }); const [codeText, setCodeText] = useState(""); const [codeSampleCodeText, setCodeSampleCodeText] = useState(() => getCodeSampleSourceFromLanguage(language) ); useEffect(() => { if (language && !!language.sample) { setCodeSampleCodeText(getCodeSampleSourceFromLanguage(language)); } if (language && !!language.options) { codegen.convert( language.language, language.variant, cleanedPostmanRequest, language.options, (error: any, snippet: string) => { if (error) { return; } setCodeText(snippet); } ); } else if (language && !language.options) { const langSource = mergedLangs.filter( (lang) => lang.language === language.language ); // Merges user-defined language with default languageSet // This allows users to define only the minimal properties necessary in languageTabs // User-defined properties should override languageSet properties const mergedLanguage = { ...langSource[0], ...language }; codegen.convert( mergedLanguage.language, mergedLanguage.variant, cleanedPostmanRequest, mergedLanguage.options, (error: any, snippet: string) => { if (error) { return; } setCodeText(snippet); } ); } else { setCodeText(""); } }, [ accept, body, contentType, cookieParams, headerParams, language, pathParams, postman, queryParams, server, cleanedPostmanRequest, mergedLangs, ]); // no dependencies was intentionally set for this particular hook. it's safe as long as if conditions are set useEffect(function onSelectedVariantUpdate() { if (selectedVariant && selectedVariant !== language?.variant) { codegen.convert( language.language, selectedVariant, cleanedPostmanRequest, language.options, (error: any, snippet: string) => { if (error) { return; } setCodeText(snippet); } ); } }); // no dependencies was intentionally set for this particular hook. it's safe as long as if conditions are set // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(function onSelectedSampleUpdate() { if ( language && language.samples && language.samplesSources && selectedSample && selectedSample !== language.sample ) { const sampleIndex = language.samples.findIndex( (smp) => smp === selectedSample ); setCodeSampleCodeText(language.samplesSources[sampleIndex]); } }); if (language === undefined) { return null; } return ( <> {/* Outer language tabs */} {mergedLangs.map((lang) => { return ( {/* Inner x-codeSamples tabs */} {lang.samples && ( {lang.samples.map((sample, index) => { return ( {/* @ts-ignore */} {codeSampleCodeText} ); })} )} {/* Inner generated code snippets */} {lang.variants.map((variant, index) => { return ( {/* @ts-ignore */} {codeText} ); })} ); })} ); } export default CodeSnippets;