Skip to content

Commit

Permalink
Add support for webhook pages (#44)
Browse files Browse the repository at this point in the history
* Upgrade @fern-fern/registry-browser

* Init webhook context

* Add hoveredPayloadPropertyPath to webhook context

* Init WebhookExample

* Init WebhookPayloadSection

* Init WebhookSection

* Implement WebhookContent

* Implement Webhook component

* Render webhooks in ApiPackageContents

* Server-side changes to accommodate webhooks

* More fixes

* Remove console statements

* Fix anchors

* Implement WebhookResponseSection

* Remove webhook description container div

* Implement WebhookHeadersSection

* Add headers to WebhookContent
  • Loading branch information
kafkas authored Aug 25, 2023
1 parent 71b4061 commit e7bba47
Show file tree
Hide file tree
Showing 27 changed files with 602 additions and 31 deletions.
10 changes: 5 additions & 5 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
2 changes: 1 addition & 1 deletion packages/ui/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@blueprintjs/icons": "^4.4.0",
"@blueprintjs/popover2": "^1.8.0",
"@blueprintjs/select": "^4.4.2",
"@fern-fern/registry-browser": "0.14.1-1-gf6c42e8",
"@fern-fern/registry-browser": "0.14.1-3-g6c09dff",
"@fern-ui/core-utils": "workspace:*",
"@fern-ui/react-commons": "workspace:*",
"@fern-ui/routing-utils": "workspace:*",
Expand Down
21 changes: 20 additions & 1 deletion packages/ui/app/src/ResolvedUrlPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ export type ResolvedUrlPath =
| ResolvedUrlPath.Api
| ResolvedUrlPath.ClientLibraries
| ResolvedUrlPath.TopLevelEndpoint
| ResolvedUrlPath.TopLevelWebhook
| ResolvedUrlPath.ApiSubpackage
| ResolvedUrlPath.Endpoint;
| ResolvedUrlPath.Endpoint
| ResolvedUrlPath.Webhook;

export declare namespace ResolvedUrlPath {
export interface Section {
Expand Down Expand Up @@ -48,6 +50,14 @@ export declare namespace ResolvedUrlPath {
endpoint: FernRegistryApiRead.EndpointDefinition;
}

export interface TopLevelWebhook {
type: "topLevelWebhook";
apiSection: FernRegistryDocsRead.ApiSection;
apiSlug: string;
slug: string;
webhook: FernRegistryApiRead.WebhookDefinition;
}

export interface ApiSubpackage {
type: "apiSubpackage";
apiSection: FernRegistryDocsRead.ApiSection;
Expand All @@ -64,4 +74,13 @@ export declare namespace ResolvedUrlPath {
endpoint: FernRegistryApiRead.EndpointDefinition;
parent: FernRegistryApiRead.ApiDefinitionSubpackage;
}

export interface Webhook {
type: "webhook";
apiSection: FernRegistryDocsRead.ApiSection;
apiSlug: string;
slug: string;
webhook: FernRegistryApiRead.WebhookDefinition;
parent: FernRegistryApiRead.ApiDefinitionSubpackage;
}
}
14 changes: 12 additions & 2 deletions packages/ui/app/src/api-page/ApiPackageContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useApiDefinitionContext } from "../api-context/useApiDefinitionContext"
import { joinUrlSlugs } from "../docs-context/joinUrlSlugs";
import { Endpoint } from "./endpoints/Endpoint";
import { ApiSubpackage } from "./subpackages/ApiSubpackage";
import { doesSubpackageHaveEndpointsRecursive } from "./subpackages/doesSubpackageHaveEndpointsRecursive";
import { doesSubpackageHaveEndpointsOrWebhooksRecursive } from "./subpackages/doesSubpackageHaveEndpointsOrWebhooksRecursive";
import { Webhook } from "./webhooks/Webhook";

export declare namespace ApiPackageContents {
export interface Props {
Expand Down Expand Up @@ -31,8 +32,17 @@ export const ApiPackageContents: React.FC<ApiPackageContents.Props> = ({
package={package_}
/>
))}
{package_.webhooks.map((webhook, idx) => (
<Webhook
key={webhook.id}
webhook={webhook}
isLastInApi={isLastInParentPackage && idx === package_.webhooks.length - 1}
slug={joinUrlSlugs(slug, webhook.urlSlug)}
package={package_}
/>
))}
{package_.subpackages.map((subpackageId, idx) => {
if (!doesSubpackageHaveEndpointsRecursive(subpackageId, resolveSubpackageById)) {
if (!doesSubpackageHaveEndpointsOrWebhooksRecursive(subpackageId, resolveSubpackageById)) {
return null;
}
const subpackage = resolveSubpackageById(subpackageId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as FernRegistryApiRead from "@fern-fern/registry-browser/api/resources/api/resources/v1/resources/read";

export function doesSubpackageHaveEndpointsRecursive(
export function doesSubpackageHaveEndpointsOrWebhooksRecursive(
subpackageId: FernRegistryApiRead.SubpackageId,
resolveSubpackage: (subpackageId: FernRegistryApiRead.SubpackageId) => FernRegistryApiRead.ApiDefinitionSubpackage
): boolean {
const subpackage = resolveSubpackage(subpackageId);
if (subpackage.endpoints.length > 0) {
if (subpackage.endpoints.length > 0 || subpackage.webhooks.length > 0) {
return true;
}
return subpackage.subpackages.some((s) => doesSubpackageHaveEndpointsRecursive(s, resolveSubpackage));
return subpackage.subpackages.some((s) => doesSubpackageHaveEndpointsOrWebhooksRecursive(s, resolveSubpackage));
}
28 changes: 28 additions & 0 deletions packages/ui/app/src/api-page/webhooks/Webhook.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as FernRegistryApiRead from "@fern-fern/registry-browser/api/resources/api/resources/v1/resources/read";
import { useApiPageCenterElement } from "../useApiPageCenterElement";
import { WebhookContextProvider } from "./webhook-context/WebhookContextProvider";
import { WebhookContent } from "./WebhookContent";

export declare namespace Webhook {
export interface Props {
webhook: FernRegistryApiRead.WebhookDefinition;
isLastInApi: boolean;
package: FernRegistryApiRead.ApiDefinitionPackage;
slug: string;
}
}

export const Webhook: React.FC<Webhook.Props> = ({ webhook, slug, package: package_, isLastInApi }) => {
const { setTargetRef } = useApiPageCenterElement({ slug });

return (
<WebhookContextProvider>
<WebhookContent
webhook={webhook}
setContainerRef={setTargetRef}
package={package_}
hideBottomSeparator={isLastInApi}
/>
</WebhookContextProvider>
);
};
144 changes: 144 additions & 0 deletions packages/ui/app/src/api-page/webhooks/WebhookContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import * as FernRegistryApiRead from "@fern-fern/registry-browser/api/resources/api/resources/v1/resources/read";
import useSize from "@react-hook/size";
import classNames from "classnames";
import { snakeCase } from "lodash-es";
import React, { useCallback, useRef } from "react";
import { isSubpackage } from "../../util/package";
import { JsonPropertyPath } from "../examples/json-example/contexts/JsonPropertyPath";
import { Markdown } from "../markdown/Markdown";
import { ApiPageMargins } from "../page-margins/ApiPageMargins";
import { SubpackageTitle } from "../subpackages/SubpackageTitle";
import { useWebhookContext } from "./webhook-context/useWebhookContext";
import { WebhookExample } from "./webhook-examples/WebhookExample";
import { WebhookHeadersSection } from "./WebhookHeadersSection";
import { WebhookPayloadSection } from "./WebhookPayloadSection";
import { WebhookResponseSection } from "./WebhookResponseSection";
import { WebhookSection } from "./WebhookSection";

export declare namespace WebhookContent {
export interface Props {
webhook: FernRegistryApiRead.WebhookDefinition;
package: FernRegistryApiRead.ApiDefinitionPackage;
hideBottomSeparator?: boolean;
setContainerRef: (ref: HTMLElement | null) => void;
}
}

export const WebhookContent = React.memo<WebhookContent.Props>(function WebhookContent({
webhook,
package: package_,
hideBottomSeparator = false,
setContainerRef,
}) {
const { setHoveredPayloadPropertyPath } = useWebhookContext();
const onHoverPayloadProperty = useCallback(
(jsonPropertyPath: JsonPropertyPath, { isHovering }: { isHovering: boolean }) => {
setHoveredPayloadPropertyPath(isHovering ? jsonPropertyPath : undefined);
},
[setHoveredPayloadPropertyPath]
);

const computeAnchor = useCallback(
(
attributeType: "payload" | "response",
attribute?:
| FernRegistryApiRead.ObjectProperty
| FernRegistryApiRead.PathParameter
| FernRegistryApiRead.QueryParameter
) => {
let anchor = "";
if (isSubpackage(package_)) {
anchor += snakeCase(package_.urlSlug) + "_";
}
anchor += snakeCase(webhook.id);
anchor += "-" + attributeType;
if (attribute?.key != null) {
anchor += "-" + snakeCase(attribute.key);
}
return anchor;
},
[package_, webhook]
);

const titleSectionRef = useRef<null | HTMLDivElement>(null);
const [, titleSectionHeight] = useSize(titleSectionRef);

const example = webhook.examples[0]; // TODO: Need a way to show all the examples

const webhookExample = example ? <WebhookExample example={example} /> : null;

return (
<ApiPageMargins
className={classNames("pb-20", {
"border-border-default-light dark:border-border-default-dark border-b": !hideBottomSeparator,
})}
>
<div className="flex min-w-0 flex-1 flex-col lg:flex-row lg:space-x-[4vw]" ref={setContainerRef}>
<div className="flex min-w-0 max-w-2xl flex-1 flex-col">
<div className="pb-8 pt-20" ref={titleSectionRef}>
{isSubpackage(package_) && (
<div className="text-accent-primary mb-4 text-xs font-semibold uppercase tracking-wider">
<SubpackageTitle subpackage={package_} />
</div>
)}
<div className="typography-font-heading text-text-primary-light dark:text-text-primary-dark text-3xl font-bold">
{webhook.name}
</div>
</div>
{webhook.description != null && <Markdown>{webhook.description}</Markdown>}

{webhook.headers.length > 0 && (
<div className="mt-8 flex">
<div className="flex flex-1 flex-col gap-12">
<WebhookSection title="Headers" anchor={computeAnchor("payload")}>
<WebhookHeadersSection webhook={webhook} />
</WebhookSection>
</div>
</div>
)}

<div className="mt-8 flex">
<div className="flex flex-1 flex-col gap-12">
<WebhookSection title="Payload" anchor={computeAnchor("payload")}>
<WebhookPayloadSection
payload={webhook.payload}
onHoverProperty={onHoverPayloadProperty}
getPropertyAnchor={(property) => computeAnchor("payload", property)}
/>
</WebhookSection>
</div>
</div>

<div className="mt-8 flex">
<div className="flex flex-1 flex-col gap-12">
<WebhookSection title="Response" anchor={computeAnchor("response")}>
<WebhookResponseSection />
</WebhookSection>
</div>
</div>
</div>
{titleSectionHeight > 0 && (
<div
className={classNames(
"flex-1 sticky self-start top-0 min-w-sm max-w-lg",
// the py-10 is the same as the 40px below
"py-10",
// the 4rem is the same as the h-10 as the Header
"max-h-[calc(100vh-4rem)]",
// hide on mobile,
"hidden lg:flex"
)}
style={{
// the 40px is the same as the py-10 above
marginTop: titleSectionHeight - 40,
}}
>
{webhookExample}
</div>
)}

<div className="mt-10 flex max-h-[150vh] lg:mt-0 lg:hidden">{webhookExample}</div>
</div>
</ApiPageMargins>
);
});
36 changes: 36 additions & 0 deletions packages/ui/app/src/api-page/webhooks/WebhookHeadersSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type * as FernRegistryApiRead from "@fern-fern/registry-browser/api/resources/api/resources/v1/resources/read";
import { MonospaceText } from "../../commons/monospace/MonospaceText";
import { Markdown } from "../markdown/Markdown";
import { TypeReferenceDefinitions } from "../types/type-reference/TypeReferenceDefinitions";
import { TypeShorthand } from "../types/type-shorthand/TypeShorthand";
import { TypeComponentSeparator } from "../types/TypeComponentSeparator";

export declare namespace WebhookHeadersSection {
export interface Props {
webhook: FernRegistryApiRead.WebhookDefinition;
}
}

export const WebhookHeadersSection: React.FC<WebhookHeadersSection.Props> = ({ webhook }) => {
return (
<div className="flex flex-col">
{webhook.headers.map((header, index) => (
<div className="flex flex-col" key={index}>
<TypeComponentSeparator />
<div className="group/anchor-container relative flex flex-col gap-2 py-3">
<div className="flex items-baseline gap-1">
<MonospaceText className="text-text-primary-light dark:text-text-primary-dark">
{header.key}
</MonospaceText>
<div className="t-muted text-xs">
<TypeShorthand type={header.type} plural={false} />
</div>
</div>
{header.description != null && <Markdown>{header.description}</Markdown>}
<TypeReferenceDefinitions type={header.type} isCollapsible />
</div>
</div>
))}
</div>
);
};
53 changes: 53 additions & 0 deletions packages/ui/app/src/api-page/webhooks/WebhookPayloadSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as FernRegistryApiRead from "@fern-fern/registry-browser/api/resources/api/resources/v1/resources/read";
import { visitDiscriminatedUnion } from "@fern-ui/core-utils";
import { JsonPropertyPath } from "../examples/json-example/contexts/JsonPropertyPath";
import { TypeDefinition } from "../types/type-definition/TypeDefinition";
import { TypeReferenceDefinitions } from "../types/type-reference/TypeReferenceDefinitions";
import { TypeShorthand } from "../types/type-shorthand/TypeShorthand";

export declare namespace WebhookPayloadSection {
export interface Props {
payload: FernRegistryApiRead.WebhookPayload;
onHoverProperty?: (path: JsonPropertyPath, opts: { isHovering: boolean }) => void;
getPropertyAnchor?: (property: FernRegistryApiRead.ObjectProperty) => string;
}
}

export const WebhookPayloadSection: React.FC<WebhookPayloadSection.Props> = ({
payload,
onHoverProperty,
getPropertyAnchor,
}) => {
return (
<div className="flex flex-col">
<div className="t-muted border-border-default-light dark:border-border-default-dark border-b pb-5 text-sm leading-6">
{"The payload of this webhook request is "}
{visitDiscriminatedUnion(payload.type, "type")._visit<JSX.Element | string>({
object: () => "an object",
reference: (type) => <TypeShorthand type={type.value} plural={false} withArticle />,
_other: () => "unknown",
})}
.
</div>
{visitDiscriminatedUnion(payload.type, "type")._visit({
object: (object) => (
<TypeDefinition
typeShape={object}
isCollapsible={false}
onHoverProperty={onHoverProperty}
getPropertyAnchor={getPropertyAnchor}
/>
),
reference: (type) => (
<TypeReferenceDefinitions
type={type.value}
isCollapsible={false}
onHoverProperty={onHoverProperty}
getPropertyAnchor={getPropertyAnchor}
/>
),
_other: () => null,
})}
</div>
);
};
Loading

0 comments on commit e7bba47

Please sign in to comment.