Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/UnikraftCodeTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
Children,
cloneElement,
isValidElement,
lazy,
Suspense,
type ReactElement,
type ReactNode,
} from "react";
import { CodeTabPanel, type CodeTabPanelProps } from "zudoku/ui/CodeTabs";
import {
ORG_PLACEHOLDER,
UNIKRAFT_TITLE,
useOrganizationName,
} from "./personalization/controlplane";

// Lazy-loaded to keep Shiki out of the initial bundle, mirroring Zudoku's own
// default CodeTabs registration.
const CodeTabs = lazy(() =>
import("zudoku/ui/CodeTabs").then((m) => ({ default: m.CodeTabs })),
);

interface UnikraftCodeTabsProps {
children?: ReactNode;
syncKey?: string;
hideIcon?: boolean;
}

const isCodeTabPanel = (
child: ReactNode,
): child is ReactElement<CodeTabPanelProps> =>
isValidElement(child) &&
(child.type as typeof CodeTabPanel).displayName === CodeTabPanel.displayName;

/** Extracts the `title="..."` value from a fenced block's meta string. */
const titleFromMeta = (meta?: string): string | undefined =>
meta?.match(/title="([^"]*)"/)?.[1];

/**
* Drop-in replacement for Zudoku's `<CodeTabs>` that personalizes Unikraft CLI
* snippets for signed-in users.
*
* Every `title="unikraft"` panel containing the `<my-org>` placeholder is
* rewritten with the user's actual organization slug. Legacy `kraft` panels,
* any other code, and the signed-out experience are left untouched, so the
* feature degrades gracefully when the user is not signed in or the
* organization cannot be resolved.
*/
export const UnikraftCodeTabs = ({
children,
...props
}: UnikraftCodeTabsProps) => {
const organizationName = useOrganizationName();

const personalizedChildren = organizationName
? Children.map(children, (child) => {
if (!isCodeTabPanel(child)) return child;

const { code, meta, title } = child.props;
const panelTitle = title ?? titleFromMeta(meta);

if (
panelTitle !== UNIKRAFT_TITLE ||
typeof code !== "string" ||
!code.includes(ORG_PLACEHOLDER)
) {
return child;
}

return cloneElement(child, {
code: code.replaceAll(ORG_PLACEHOLDER, organizationName),
});
})
: children;

return (
<Suspense>
<CodeTabs {...props}>{personalizedChildren}</CodeTabs>
</Suspense>
);
};

export default UnikraftCodeTabs;
72 changes: 72 additions & 0 deletions src/personalization/controlplane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useAuth, useZudoku } from "zudoku/hooks";
import { useQuery } from "zudoku/react-query";

/**
* Base URL of the Unikraft Cloud control plane API.
*
* The personalization feature reads the signed-in user's organization from
* `GET {CONTROLPLANE_URL}/v1/auth`, authenticated with the user's OIDC access
* token. The token must carry the `org:metadata` scope, so the OIDC login
* configured in `zudoku.config.tsx` must request `scope: "openid org:metadata"`.
*/
export const CONTROLPLANE_URL = "https://cloud-console-pr-968.ukp-stable.apw.unikraft.internal";

/** Placeholder used throughout the docs to stand in for the user's org slug. */
export const ORG_PLACEHOLDER = "<my-org>";

/** Only code panels carrying this title are personalized. */
export const UNIKRAFT_TITLE = "unikraft";

/**
* Relevant part of the `GET /v1/auth` response envelope. Every response carries
* a `status`; on failure `data` is absent and `status` is `"error"`.
*/
interface GetAuthorizationResponse {
status?: "success" | "error";
message?: string;
data?: {
organization_name?: string;
organization_display_name?: string;
registry?: string;
};
}

/**
* Returns the signed-in user's organization slug, or `undefined` when the user
* is signed out, authentication is unavailable, or the request fails.
*
* The query is keyed by the user's subject so switching accounts refetches, and
* the shared key deduplicates the many code blocks rendered on a single page
* into a single request.
*/
export const useOrganizationName = (): string | undefined => {
const { isAuthEnabled, isAuthenticated, profile } = useAuth();
const { authentication } = useZudoku();

const { data } = useQuery({
queryKey: ["unikraft-organization", profile?.sub],
enabled: isAuthEnabled && isAuthenticated && Boolean(authentication),
staleTime: 5 * 60 * 1000,
retry: false,
queryFn: async (): Promise<string | undefined> => {
if (!authentication) return undefined;

const request = new Request(`${CONTROLPLANE_URL}/v1/auth`, {
headers: { Accept: "application/json" },
});
const response = await fetch(await authentication.signRequest(request));

const body = (await response
.json()
.catch(() => undefined)) as GetAuthorizationResponse | undefined;

// The API wraps results in a status envelope; treat any non-success
// response as failure regardless of the HTTP status code.
if (body?.status !== "success") return undefined;

return body.data?.organization_name || undefined;
},
});

return data ?? undefined;
};
64 changes: 43 additions & 21 deletions zudoku.config.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import type { ZudokuConfig } from "zudoku";
import { UnikraftCodeTabs } from "./src/UnikraftCodeTabs";

const OIDC_CLIENT_ID = process.env.ZUDOKU_OIDC_CLIENT_ID;
const OIDC_ISSUER = process.env.ZUDOKU_OIDC_ISSUER;

const config: ZudokuConfig = {
metadata: {
title: "%s | Unikraft Cloud Docs",
description:
"Unikraft Cloud documentation: guides, platform reference, CLI, and API docs for the millisecond, Linux-based microVM cloud platform.",
favicon: "/docs/favicon.ico",
},
basePath: "/docs",
// Emit <link rel="canonical"> on every docs page (origin + basePath + route),
// and generate a sitemap so docs pages are discoverable/indexable by agents.
canonicalUrlOrigin: "https://unikraft.com",
sitemap: {
siteUrl: "https://unikraft.com",
},
site: {
logo: {
src: { light: "/logo-light.svg", dark: "/logo-dark.svg" },
Expand All @@ -22,6 +18,22 @@ const config: ZudokuConfig = {
},
showPoweredBy: false,
},
...(OIDC_CLIENT_ID && OIDC_ISSUER
? {
authentication: {
type: "openid" as const,
clientId: OIDC_CLIENT_ID,
issuer: OIDC_ISSUER,
scopes: [
"openid",
"profile",
"email",
"docs:enterprise",
"org:metadata",
],
},
}
: {}),
docs: {
files: "/pages/**/*.{md,mdx}",
defaultOptions: {
Expand Down Expand Up @@ -56,6 +68,12 @@ const config: ZudokuConfig = {
dark: "github-dark-high-contrast",
},
},
mdx: {
components: {
// Personalizes `<my-org>` in Unikraft CLI code tabs for signed-in users.
CodeTabs: UnikraftCodeTabs,
},
},
navigation: [
{
type: "category",
Expand Down Expand Up @@ -88,11 +106,6 @@ const config: ZudokuConfig = {
"/features/cron-jobs",
"/features/forking",
"/features/branching",
"/features/managed-volumes",
"/features/checkpoints",
"/features/plugins",
"/features/custom-network-configuration",
"/features/annotations",
],
},
{
Expand Down Expand Up @@ -187,8 +200,6 @@ const config: ZudokuConfig = {
"/guides/memcached1.6", // Memcached
"/guides/minio", // Minio
"/guides/mongodb", // MongoDB
"/guides/mysql", // MySQL
"/guides/neo4j", // Neo4j
"/guides/httpserver-node21-nextjs", // Next.js HTTP Server
"/guides/nginx", // Nginx
"/guides/node24-karaoke", // Node AllKaraoke
Expand Down Expand Up @@ -315,6 +326,7 @@ const config: ZudokuConfig = {
collapsed: false,
items: [
"/cli/unikraft/images",
"/cli/unikraft/images/build",
"/cli/unikraft/images/copy",
"/cli/unikraft/images/delete",
"/cli/unikraft/images/get",
Expand Down Expand Up @@ -400,9 +412,11 @@ const config: ZudokuConfig = {
collapsed: false,
items: [
"/cli/unikraft/volumes",
"/cli/unikraft/volumes/attach",
"/cli/unikraft/volumes/clone",
"/cli/unikraft/volumes/create",
"/cli/unikraft/volumes/delete",
"/cli/unikraft/volumes/detach",
"/cli/unikraft/volumes/edit",
"/cli/unikraft/volumes/get",
"/cli/unikraft/volumes/import",
Expand Down Expand Up @@ -506,6 +520,20 @@ const config: ZudokuConfig = {
"/cli/kraft/metro/list",
],
},
{
type: "category",
label: "kraft cloud scale",
icon: "arrow-up-1-0",
collapsed: false,
items: [
"/cli/kraft/scale",
"/cli/kraft/scale/add",
"/cli/kraft/scale/get",
"/cli/kraft/scale/init",
"/cli/kraft/scale/remove",
"/cli/kraft/scale/reset",
],
},
{
type: "category",
label: "kraft cloud service",
Expand Down Expand Up @@ -563,12 +591,6 @@ const config: ZudokuConfig = {
icon: "unplug",
to: "/api/platform/v1",
},
{
type: "link",
label: "Glossary",
icon: "book-a",
to: "https://unikraft.com/glossary",
},
],
search: {
type: "pagefind",
Expand Down
Loading