Skip to content

Commit

Permalink
feat: isUnathenticatedPagesDiscoverable feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity committed Oct 10, 2024
1 parent 265b23e commit 349de32
Show file tree
Hide file tree
Showing 20 changed files with 171 additions and 87 deletions.
1 change: 1 addition & 0 deletions packages/fdr-sdk/src/navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export * from "../client/generated/api/resources/commons";
export * from "./ApiDefinitionHolder";
export * as migrate from "./migrators";
export * as utils from "./utils";
export * from "./utils/pruneNavigationTree";
export * from "./versions";
export { ApiDefinitionHolder, ApiDefinitionPruner, ApiTypeIdVisitor, NodeCollector };
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FernNavigation } from "../../..";
import { pruneNavigationTree } from "../pruneNavigationTree";
import { Pruner } from "../pruneNavigationTree";

describe("pruneNavigationTree", () => {
it("should not prune the tree if keep returns true for all nodes", () => {
Expand Down Expand Up @@ -32,7 +32,9 @@ describe("pruneNavigationTree", () => {
pointsTo: undefined,
};

const result = pruneNavigationTree(root, () => true);
const result = Pruner.from(root)
.keep(() => true)
.get();

// structuredClone should duplicate the object
expect(result === root).toBe(false);
Expand Down Expand Up @@ -97,7 +99,9 @@ describe("pruneNavigationTree", () => {
pointsTo: FernNavigation.Slug("root/page"),
};

const result = pruneNavigationTree(root, (node) => node.id !== FernNavigation.NodeId("page"));
const result = Pruner.from(root)
.keep((node) => node.id !== FernNavigation.NodeId("page"))
.get();

expect(result).toBeUndefined();
});
Expand Down Expand Up @@ -132,7 +136,9 @@ describe("pruneNavigationTree", () => {
pointsTo: undefined,
};

const result = pruneNavigationTree(root, (node) => node.id !== "root");
const result = Pruner.from(root)
.keep((node) => node.id !== "root")
.get();

// structuredClone should duplicate the object
expect(result === root).toBe(false);
Expand Down Expand Up @@ -197,7 +203,9 @@ describe("pruneNavigationTree", () => {
pointsTo: undefined,
};

const result = pruneNavigationTree(root, (node) => node.id !== "page");
const result = Pruner.from(root)
.keep((node) => node.id !== "page")
.get();

// structuredClone should duplicate the object
expect(result === root).toBe(false);
Expand Down Expand Up @@ -249,7 +257,9 @@ describe("pruneNavigationTree", () => {
pointsTo: undefined,
};

const result = pruneNavigationTree(root, (node) => node.id !== "root");
const result = Pruner.from(root)
.keep((node) => node.id !== "root")
.get();

// structuredClone should duplicate the object
expect(result === root).toBe(false);
Expand Down Expand Up @@ -342,7 +352,9 @@ describe("pruneNavigationTree", () => {
pointsTo: undefined,
};

const result = pruneNavigationTree(root, (node) => node.id !== "page1");
const result = Pruner.from(root)
.keep((node) => node.id !== "page1")
.get();

// structuredClone should duplicate the object
expect(result === root).toBe(false);
Expand Down
1 change: 0 additions & 1 deletion packages/fdr-sdk/src/navigation/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ export * from "./createBreadcrumbs";
export * from "./findNode";
export * from "./getApiReferenceId";
export * from "./getNoIndexFromFrontmatter";
export * from "./pruneNavigationTree";
export * from "./toRootNode";
export * from "./toUnversionedSlug";
98 changes: 57 additions & 41 deletions packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,71 @@
import structuredClone from "@ungap/structured-clone";
import { DeepReadonly } from "ts-essentials";
import { FernNavigation } from "../..";
import { bfs } from "../../utils/traversers/bfs";
import { prunetree } from "../../utils/traversers/prunetree";
import { mutableDeleteChild } from "./deleteChild";
import { mutableUpdatePointsTo } from "./updatePointsTo";

/**
* @param root the root node of the navigation tree
* @param keep a function that returns true if the node should be kept
* @param hide a function that returns true if the node should be hidden
* @returns a new navigation tree with only the nodes that should be kept
*/
export function pruneNavigationTree<ROOT extends FernNavigation.NavigationNode>(
root: DeepReadonly<ROOT>,
keep?: (node: FernNavigation.NavigationNode) => boolean,
hide?: (node: FernNavigation.NavigationNodeWithMetadata) => boolean,
): ROOT | undefined {
const clone = structuredClone(root) as ROOT;
return mutablePruneNavigationTree(clone, keep, hide);
}
export class Pruner<ROOT extends FernNavigation.NavigationNode> {
public static from<ROOT extends FernNavigation.NavigationNode>(tree: ROOT): Pruner<ROOT> {
return new Pruner(tree);
}

function mutablePruneNavigationTree<ROOT extends FernNavigation.NavigationNode>(
root: ROOT,
keep: (node: FernNavigation.NavigationNode) => boolean = () => true,
hide: (node: FernNavigation.NavigationNodeWithMetadata) => boolean = () => false,
): ROOT | undefined {
const [result] = prunetree(root, {
predicate: keep,
getChildren: FernNavigation.getChildren,
getPointer: (node) => node.id,
deleter: mutableDeleteChild,
});
private tree: ROOT | undefined;
private constructor(tree: ROOT) {
this.tree = structuredClone(tree) as ROOT;
}

if (result == null) {
return undefined;
public keep(predicate: (node: FernNavigation.NavigationNode) => boolean): this {
if (this.tree == null) {
return this;
}
const [result] = prunetree(this.tree, {
predicate,
getChildren: FernNavigation.getChildren,
getPointer: (node) => node.id,
deleter: mutableDeleteChild,
});
this.tree = result;
return this;
}

// since the tree has been pruned, we need to update the pointsTo property
mutableUpdatePointsTo(result);
public hide(predicate: (node: FernNavigation.NavigationNodeWithMetadata) => boolean): this {
if (this.tree == null) {
return this;
}
bfs(
this.tree,
(node) => {
if (FernNavigation.hasMarkdown(node) && predicate(node)) {
node.hidden = true;
}
},
FernNavigation.getChildren,
);
return this;
}

// other operations
bfs(
result,
(node) => {
if (FernNavigation.hasMarkdown(node) && hide(node)) {
node.hidden = true;
}
},
FernNavigation.getChildren,
);
public authed(predicate: (node: FernNavigation.NavigationNodeWithMetadata) => boolean): this {
if (this.tree == null) {
return this;
}
bfs(
this.tree,
(node) => {
if (FernNavigation.hasMarkdown(node) && predicate(node)) {
node.authed = true;
}
},
FernNavigation.getChildren,
);
return this;
}

return result;
public get(): ROOT | undefined {
if (this.tree == null) {
return undefined;
}
mutableUpdatePointsTo(this.tree);
return this.tree;
}
}
14 changes: 12 additions & 2 deletions packages/ui/app/src/sidebar/SidebarLink.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { FernTooltip, RemoteFontAwesomeIcon } from "@fern-ui/components";
import cn, { clsx } from "clsx";
import { NavArrowDown } from "iconoir-react";
import { Lock, NavArrowDown } from "iconoir-react";
import { range } from "lodash-es";
import { Url } from "next/dist/shared/lib/router/router";
import {
Expand Down Expand Up @@ -42,6 +42,7 @@ interface SidebarSlugLinkProps {
rightElement?: ReactNode;
tooltipContent?: ReactNode;
hidden?: boolean;
authed?: boolean;
as?: keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
}

Expand Down Expand Up @@ -76,6 +77,7 @@ const SidebarLinkInternal = forwardRef<HTMLDivElement, SidebarLinkProps>((props,
target,
rel,
hidden,
authed,
as = "span",
} = props;

Expand Down Expand Up @@ -119,6 +121,14 @@ const SidebarLinkInternal = forwardRef<HTMLDivElement, SidebarLinkProps>((props,
};

const withTooltip = (content: ReactNode) => {
if (authed) {
return (
<FernTooltip content="You must be logged in to view this page" side="right">
{content}
</FernTooltip>
);
}

if (tooltipContent == null) {
return content;
}
Expand Down Expand Up @@ -172,7 +182,7 @@ const SidebarLinkInternal = forwardRef<HTMLDivElement, SidebarLinkProps>((props,
</span>
)}
{createElement(as, { className: "fern-sidebar-link-text" }, title)}
{rightElement}
{authed ? <Lock className="size-4 self-center text-faded" /> : rightElement}
</span>
{expandButton}
</>,
Expand Down
1 change: 1 addition & 0 deletions packages/ui/app/src/sidebar/nodes/SidebarApiLeafNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function SidebarApiLeafNode({ node, depth, shallow }: SidebarApiLeafNodeP
title={node.title}
depth={Math.max(0, depth - 1)}
hidden={node.hidden}
authed={node.authed}
icon={renderRightElement()}
selected={selected}
shallow={shallow}
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/app/src/sidebar/nodes/SidebarApiPackageNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function SidebarApiPackageNode({
slug={node.slug}
depth={Math.max(depth - 1, 0)}
title={node.title}
authed={node.authed}
selected={selected}
icon={node.icon}
hidden={node.hidden}
Expand Down Expand Up @@ -93,6 +94,7 @@ export function SidebarApiPackageNode({
toggleExpand={handleToggleExpand}
showIndicator={showIndicator}
hidden={node.hidden}
authed={node.authed}
slug={node.overviewPageId != null ? node.slug : undefined}
selected={selected}
shallow={shallow}
Expand Down
1 change: 1 addition & 0 deletions packages/ui/app/src/sidebar/nodes/SidebarChangelogNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function SidebarChangelogNode({ node, depth, className }: SidebarChangelo
icon={node.icon ?? <Calendar className="size-4" />}
tooltipContent={renderChangelogTooltip(node)}
hidden={node.hidden}
authed={node.authed}
/>
);
}
Expand Down
12 changes: 11 additions & 1 deletion packages/ui/app/src/sidebar/nodes/SidebarLinkNode.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { OpenNewWindow } from "iconoir-react";
import { useEffect, useState } from "react";
import { SidebarLink } from "../SidebarLink";

interface SidebarLinkNodeProps {
Expand All @@ -9,14 +10,23 @@ interface SidebarLinkNodeProps {
}

export function SidebarLinkNode({ node, depth, className }: SidebarLinkNodeProps): React.ReactElement {
// TODO: handle this more gracefully, and make this SSG-friendly
const [origin, setOrigin] = useState<string>("xxx");
useEffect(() => {
setOrigin(window.location.origin);
}, []);

return (
<SidebarLink
icon={node.icon}
nodeId={node.id}
className={className}
depth={Math.max(depth - 1, 0)}
title={node.title}
rightElement={node.url.startsWith("http") && <OpenNewWindow className="size-4 self-center text-faded" />}
rightElement={
node.url.startsWith("http") &&
!node.url.startsWith(origin) && <OpenNewWindow className="size-4 self-center text-faded" />
}
href={node.url}
/>
);
Expand Down
1 change: 1 addition & 0 deletions packages/ui/app/src/sidebar/nodes/SidebarPageNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function SidebarPageNode({ node, depth, className, shallow }: SidebarPage
selected={selected}
icon={node.icon}
hidden={node.hidden}
authed={node.authed}
shallow={shallow}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function SidebarRootApiPackageNode({
selected={selected}
icon={node.icon}
hidden={node.hidden}
authed={node.authed}
shallow={shallow}
/>
);
Expand Down
1 change: 1 addition & 0 deletions packages/ui/app/src/sidebar/nodes/SidebarRootHeading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function SidebarRootHeading({ node, className, shallow }: SidebarRootHead
className={className}
title={node.title}
hidden={node.hidden}
authed={node.authed}
slug={node.slug}
selected={selected}
shallow={shallow}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function SidebarRootSectionNode({ node, className }: SidebarRootSectionNo
selected={selected}
icon={node.icon}
hidden={node.hidden}
authed={node.authed}
/>
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/ui/app/src/sidebar/nodes/SidebarSectionNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export function SidebarSectionNode({ node, className, depth }: SidebarSectionNod
toggleExpand={handleToggleExpand}
showIndicator={showIndicator}
hidden={node.hidden}
authed={node.authed}
slug={node.overviewPageId != null ? node.slug : undefined}
selected={selected}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/docs-bundle/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const middleware: NextMiddleware = async (request) => {
* redirect to the custom auth provider
*/
if (!isLoggedIn && authConfig?.type === "basic_token_verification") {
if (!withBasicTokenAnonymous(authConfig, pathname)) {
if (withBasicTokenAnonymous(authConfig, pathname)) {
const destination = new URL(authConfig.redirect);
destination.searchParams.set("state", urlJoin(withDefaultProtocol(xFernHost), pathname));
// TODO: validate allowlist of domains to prevent open redirects
Expand Down
Loading

0 comments on commit 349de32

Please sign in to comment.