diff --git a/gatsby/__tests__/cloud-plan.test.js b/gatsby/__tests__/cloud-plan.test.js new file mode 100644 index 00000000..ffbd9fed --- /dev/null +++ b/gatsby/__tests__/cloud-plan.test.js @@ -0,0 +1,54 @@ +jest.mock("../toc", () => ({})); +jest.mock("../path", () => ({})); +jest.mock("../toc-filter", () => ({})); +jest.mock("../url-resolver", () => ({})); + +const { determineInDefaultPlan } = require("../cloud-plan"); +const { CloudPlan, Repo, Locale } = require("../../src/shared/interface"); + +const pathConfig = { + locale: Locale.en, + repo: Repo.tidbcloud, + branch: "master", + version: null, +}; + +describe("cloud plan detection", () => { + it("detects BYOC-only TiDB Cloud articles", () => { + const tocMap = new Map([ + [ + "en/tidbcloud/master", + { + dedicated: new Set(), + starter: new Set(), + essential: new Set(), + premium: new Set(), + byoc: new Set(["byoc-overview"]), + }, + ], + ]); + + expect(determineInDefaultPlan("byoc-overview", pathConfig, tocMap)).toBe( + CloudPlan.Byoc + ); + }); + + it("keeps dedicated as the default when a BYOC page is also in the dedicated TOC", () => { + const tocMap = new Map([ + [ + "en/tidbcloud/master", + { + dedicated: new Set(["shared-overview"]), + starter: new Set(), + essential: new Set(), + premium: new Set(), + byoc: new Set(["shared-overview"]), + }, + ], + ]); + + expect(determineInDefaultPlan("shared-overview", pathConfig, tocMap)).toBe( + CloudPlan.Dedicated + ); + }); +}); diff --git a/gatsby/__tests__/toc-filter.test.ts b/gatsby/__tests__/toc-filter.test.ts index c5c030d7..a0ab1ce1 100644 --- a/gatsby/__tests__/toc-filter.test.ts +++ b/gatsby/__tests__/toc-filter.test.ts @@ -22,6 +22,17 @@ describe("toc-filter ignore rules", () => { ).toBe(true); }); + it("does not ignore the BYOC TiDB Cloud TOC", () => { + jest.resetModules(); + const { isIgnoredTocRelativePath } = require("../toc-ignore"); + + expect( + isIgnoredTocRelativePath( + "markdown-pages/en/tidbcloud/master/TOC-tidb-cloud-byoc.md" + ) + ).toBe(false); + }); + it("supports env-based ignore substrings", () => { process.env.WEBSITE_IGNORED_TOC_RELATIVE_PATH_SUBSTRINGS = "custom/TOC-ignore-me.md, TOC-some-other.md"; diff --git a/gatsby/cloud-plan.ts b/gatsby/cloud-plan.ts index 4e519262..527b8ea6 100644 --- a/gatsby/cloud-plan.ts +++ b/gatsby/cloud-plan.ts @@ -11,12 +11,13 @@ type TocMap = Map< starter: Set; essential: Set; premium: Set; + byoc: Set; } >; /** * Get files from different TOC types for tidbcloud - * Returns a Map where key is "locale/repo/version" and value is object with dedicated, starter, essential file sets + * Returns a Map where key is "locale/repo/version" and value is object with dedicated, starter, essential, premium, and byoc file sets */ export async function getTidbCloudFilesFromTocs(graphql: any): Promise { const tocQuery = await graphql(` @@ -68,6 +69,8 @@ export async function getTidbCloudFilesFromTocs(graphql: any): Promise { tocType = CloudPlan.Essential; } else if (relativePath.includes("TOC-tidb-cloud-premium")) { tocType = CloudPlan.Premium; + } else if (relativePath.includes("TOC-tidb-cloud-byoc")) { + tocType = CloudPlan.Byoc; } // Initialize the entry if it doesn't exist @@ -77,6 +80,7 @@ export async function getTidbCloudFilesFromTocs(graphql: any): Promise { starter: new Set(), essential: new Set(), premium: new Set(), + byoc: new Set(), }); } @@ -116,7 +120,7 @@ export function determineInDefaultPlan( return null; } - const { dedicated, starter, essential, premium } = tocData; + const { dedicated, starter, essential, premium, byoc } = tocData; // Check if article is in TOC.md (dedicated) if (dedicated.has(fileName)) { @@ -146,5 +150,15 @@ export function determineInDefaultPlan( return CloudPlan.Premium; } + if ( + byoc.has(fileName) && + !premium.has(fileName) && + !essential.has(fileName) && + !dedicated.has(fileName) && + !starter.has(fileName) + ) { + return CloudPlan.Byoc; + } + return null; } diff --git a/gatsby/create-pages/create-doc-home.ts b/gatsby/create-pages/create-doc-home.ts index 751d22a4..45400a12 100644 --- a/gatsby/create-pages/create-doc-home.ts +++ b/gatsby/create-pages/create-doc-home.ts @@ -97,6 +97,7 @@ export const createDocHome = async ({ pathConfig, "tidb-cloud-essential" ); + const byocNavUrl = generateNavTOCPath(pathConfig, "tidb-cloud-byoc"); const locale = process.env.WEBSITE_BUILD_TYPE === "archive" ? [Locale.en, Locale.zh] @@ -114,6 +115,7 @@ export const createDocHome = async ({ navUrl, starterNavUrl, essentialNavUrl, + byocNavUrl, pageUrl: path, availIn: { locale, diff --git a/gatsby/create-pages/create-docs.ts b/gatsby/create-pages/create-docs.ts index eaadbf93..45a075c8 100644 --- a/gatsby/create-pages/create-docs.ts +++ b/gatsby/create-pages/create-docs.ts @@ -131,6 +131,7 @@ export const createDocs = async (createPagesArgs: CreatePagesArgs) => { pathConfig, "tidb-cloud-essential" ); + const byocNavUrl = generateNavTOCPath(pathConfig, "tidb-cloud-byoc"); const locale = [Locale.en, Locale.zh, Locale.ja] .map((l) => @@ -162,6 +163,7 @@ export const createDocs = async (createPagesArgs: CreatePagesArgs) => { navUrl, starterNavUrl, essentialNavUrl, + byocNavUrl, availIn: { locale, version: versionRecord[pathConfig.locale][pathConfig.repo][name], diff --git a/gatsby/create-types/create-navs.ts b/gatsby/create-types/create-navs.ts index 5fe431d7..59feb617 100644 --- a/gatsby/create-types/create-navs.ts +++ b/gatsby/create-types/create-navs.ts @@ -3,6 +3,39 @@ import { mdxAstToToc } from "../toc"; import { Root } from "mdast"; import { calculateFileUrl } from "../url-resolver"; +function createCloudPlanNavigationField(cacheKey: string, tocSuffix: string) { + return { + async resolve(mdxNode: any, args: unknown, context: unknown, info: any) { + if (mdxNode[cacheKey]) return mdxNode[cacheKey]; + const types = info.schema.getType("Mdx").getFields(); + const slug = await types["slug"].resolve(mdxNode, args, context, { + fieldName: "slug", + }); + + const mdxAST: Root = await types["mdxAST"].resolve( + mdxNode, + args, + context, + { + fieldName: "mdxAST", + } + ); + + if (!slug.endsWith(tocSuffix)) + throw new Error(`unsupported query in ${slug}`); + const tocPath = calculateFileUrl(slug); + const res = mdxAstToToc( + mdxAST.children, + tocPath || slug, + undefined, + true + ); + mdxNode[cacheKey] = res; + return res; + }, + }; +} + export const createNavs = ({ actions }: CreatePagesArgs) => { const { createTypes, createFieldExtension } = actions; @@ -48,82 +81,27 @@ export const createNavs = ({ actions }: CreatePagesArgs) => { createFieldExtension({ name: "starterNavigation", extend() { - return { - async resolve( - mdxNode: any, - args: unknown, - context: unknown, - info: any - ) { - if (mdxNode.starterNav) return mdxNode.starterNav; - const types = info.schema.getType("Mdx").getFields(); - const slug = await types["slug"].resolve(mdxNode, args, context, { - fieldName: "slug", - }); - - const mdxAST: Root = await types["mdxAST"].resolve( - mdxNode, - args, - context, - { - fieldName: "mdxAST", - } - ); - - if (!slug.endsWith("TOC-tidb-cloud-starter")) - throw new Error(`unsupported query in ${slug}`); - const tocPath = calculateFileUrl(slug); - const res = mdxAstToToc( - mdxAST.children, - tocPath || slug, - undefined, - true - ); - mdxNode.starterNav = res; - return res; - }, - }; + return createCloudPlanNavigationField( + "starterNav", + "TOC-tidb-cloud-starter" + ); }, }); createFieldExtension({ name: "essentialNavigation", extend() { - return { - async resolve( - mdxNode: any, - args: unknown, - context: unknown, - info: any - ) { - if (mdxNode.essentialNav) return mdxNode.essentialNav; - const types = info.schema.getType("Mdx").getFields(); - const slug = await types["slug"].resolve(mdxNode, args, context, { - fieldName: "slug", - }); - - const mdxAST: Root = await types["mdxAST"].resolve( - mdxNode, - args, - context, - { - fieldName: "mdxAST", - } - ); + return createCloudPlanNavigationField( + "essentialNav", + "TOC-tidb-cloud-essential" + ); + }, + }); - if (!slug.endsWith("TOC-tidb-cloud-essential")) - throw new Error(`unsupported query in ${slug}`); - const tocPath = calculateFileUrl(slug); - const res = mdxAstToToc( - mdxAST.children, - tocPath || slug, - undefined, - true - ); - mdxNode.essentialNav = res; - return res; - }, - }; + createFieldExtension({ + name: "byocNavigation", + extend() { + return createCloudPlanNavigationField("byocNav", "TOC-tidb-cloud-byoc"); }, }); @@ -132,6 +110,7 @@ export const createNavs = ({ actions }: CreatePagesArgs) => { navigation: JSON! @navigation starterNavigation: JSON! @starterNavigation essentialNavigation: JSON! @essentialNavigation + byocNavigation: JSON! @byocNavigation } `); }; diff --git a/gatsby/path/index.ts b/gatsby/path/index.ts index fd3c88c0..1aa51a63 100644 --- a/gatsby/path/index.ts +++ b/gatsby/path/index.ts @@ -60,6 +60,7 @@ export function generateConfig(slug: string): { CloudPlan.Starter, CloudPlan.Essential, CloudPlan.Premium, + CloudPlan.Byoc, ] as const; prefix = simplePrefixes.find((p) => slug.includes(`${p}/`)); } diff --git a/gatsby/toc-filter.ts b/gatsby/toc-filter.ts index 2d1a0e66..470f40ee 100644 --- a/gatsby/toc-filter.ts +++ b/gatsby/toc-filter.ts @@ -58,6 +58,7 @@ function sortTocNames(tocNames: Iterable): string[] { "TOC-tidb-cloud-starter", "TOC-tidb-cloud-essential", "TOC-tidb-cloud-premium", + "TOC-tidb-cloud-byoc", ]; const priorityIndex = new Map(priority.map((name, idx) => [name, idx])); return [...new Set(tocNames)].sort((a, b) => { diff --git a/gatsby/toc-ignore.ts b/gatsby/toc-ignore.ts index a9a03633..3b440a32 100644 --- a/gatsby/toc-ignore.ts +++ b/gatsby/toc-ignore.ts @@ -25,4 +25,3 @@ export function isIgnoredTocRelativePath(relativePath: string): boolean { (substr) => substr && relativePath.includes(substr) ); } - diff --git a/locale/en/translation.json b/locale/en/translation.json index 8400b5a1..1e1e9036 100644 --- a/locale/en/translation.json +++ b/locale/en/translation.json @@ -34,6 +34,7 @@ "tidbCloudEssential": "TiDB Cloud Essential", "tidbCloudDedicated": "TiDB Cloud Dedicated", "tidbCloudLake": "TiDB Cloud Lake", + "tidbCloudByoc": "TiDB Cloud BYOC", "tidbShortTerm": "TiDB", "tidbOnKubernetes": "TiDB on Kubernetes", "tidbCloudReleases": "TiDB Cloud Releases", diff --git a/locale/ja/translation.json b/locale/ja/translation.json index d7ef2133..a8c4617c 100644 --- a/locale/ja/translation.json +++ b/locale/ja/translation.json @@ -34,6 +34,7 @@ "tidbCloudEssential": "TiDB Cloud Essential", "tidbCloudDedicated": "TiDB Cloud Dedicated", "tidbCloudLake": "TiDB Cloud Lake", + "tidbCloudByoc": "TiDB Cloud BYOC", "tidbShortTerm": "TiDB", "tidbOnKubernetes": "TiDB on Kubernetes", "tidbCloudReleases": "TiDB Cloud リリース", diff --git a/locale/zh/translation.json b/locale/zh/translation.json index d1d1f200..cf47890a 100644 --- a/locale/zh/translation.json +++ b/locale/zh/translation.json @@ -32,6 +32,7 @@ "tidbCloudEssential": "TiDB Cloud Essential", "tidbCloudDedicated": "TiDB Cloud Dedicated", "tidbCloudLake": "TiDB Cloud Lake", + "tidbCloudByoc": "TiDB Cloud BYOC", "tidbShortTerm": "TiDB", "tidbOnKubernetes": "TiDB on Kubernetes", "tidbCloudReleases": "TiDB Cloud 发布记录", diff --git a/src/components/Layout/Header/HeaderNavConfigData.tsx b/src/components/Layout/Header/HeaderNavConfigData.tsx index 9a71f02b..14e81ddd 100644 --- a/src/components/Layout/Header/HeaderNavConfigData.tsx +++ b/src/components/Layout/Header/HeaderNavConfigData.tsx @@ -103,6 +103,19 @@ const getDefaultNavConfig = ( } }, }, + { + type: "item", + label: t("navbar.tidbCloudByoc"), + to: `/tidbcloud/premium/?${CLOUD_MODE_KEY}=${CloudPlan.Byoc}`, + selected: (namespace) => + namespace === TOCNamespace.TiDBCloud && + cloudPlan === CloudPlan.Byoc, + onClick: () => { + if (typeof window !== "undefined") { + sessionStorage.setItem(CLOUD_MODE_KEY, CloudPlan.Byoc); + } + }, + }, ], }, { diff --git a/src/components/Layout/VersionSelect/CloudVersionSelect.tsx b/src/components/Layout/VersionSelect/CloudVersionSelect.tsx index a5e991bf..0cee9a54 100644 --- a/src/components/Layout/VersionSelect/CloudVersionSelect.tsx +++ b/src/components/Layout/VersionSelect/CloudVersionSelect.tsx @@ -18,6 +18,7 @@ export const CLOUD_PLAN_LABEL_STRINGS: Partial> = { [CloudPlan.Dedicated]: "Dedicated", [CloudPlan.Starter]: "Starter (formerly Serverless)", [CloudPlan.Essential]: "Essential", + [CloudPlan.Byoc]: "BYOC", }; const CLOUD_VERSIONS = [ @@ -51,6 +52,10 @@ const CLOUD_VERSIONS = [ /> ), }, + { + label: "BYOC", + value: CloudPlan.Byoc, + }, ]; const VersionItems = (props: { @@ -70,6 +75,11 @@ const VersionItems = (props: { const getToUrl = (version: string) => { const searchParams = new URLSearchParams(); searchParams.set(CLOUD_MODE_KEY, version); + if (version === CloudPlan.Byoc) { + return `/${pathConfig.repo}/${ + CloudPlan.Premium + }/?${searchParams.toString()}`; + } return version === CloudPlan.Dedicated ? `/${pathConfig.repo}/` : `/${pathConfig.repo}/${version}/?${searchParams.toString()}`; diff --git a/src/shared/interface.ts b/src/shared/interface.ts index 86b60f18..7c859615 100644 --- a/src/shared/interface.ts +++ b/src/shared/interface.ts @@ -109,4 +109,5 @@ export enum CloudPlan { Starter = "starter", Essential = "essential", Premium = "premium", + Byoc = "byoc", } diff --git a/src/shared/useCloudPlan.ts b/src/shared/useCloudPlan.ts index 2185f588..1a0eaa9f 100644 --- a/src/shared/useCloudPlan.ts +++ b/src/shared/useCloudPlan.ts @@ -17,6 +17,7 @@ const TOC_NAME_TO_CLOUD_PLAN: Record = { "TOC-tidb-cloud-starter": CloudPlan.Starter, "TOC-tidb-cloud-essential": CloudPlan.Essential, "TOC-tidb-cloud-premium": CloudPlan.Premium, + "TOC-tidb-cloud-byoc": CloudPlan.Byoc, }; function isCloudPlan(value: string | null): value is CloudPlan { @@ -24,7 +25,8 @@ function isCloudPlan(value: string | null): value is CloudPlan { value === CloudPlan.Dedicated || value === CloudPlan.Starter || value === CloudPlan.Essential || - value === CloudPlan.Premium + value === CloudPlan.Premium || + value === CloudPlan.Byoc ); } @@ -97,10 +99,11 @@ export const useCloudPlan = () => { const isStarter = isTidbcloud && resolvedCloudPlan === CloudPlan.Starter; const isEssential = isTidbcloud && resolvedCloudPlan === CloudPlan.Essential; const isPremium = isTidbcloud && resolvedCloudPlan === CloudPlan.Premium; + const isByoc = isTidbcloud && resolvedCloudPlan === CloudPlan.Byoc; const isClassic = !isTidbcloud || !resolvedCloudPlan || - (!isStarter && !isEssential); + (!isStarter && !isEssential && !isByoc); return { cloudPlan: resolvedCloudPlan, @@ -108,6 +111,7 @@ export const useCloudPlan = () => { isStarter, isEssential, isPremium, + isByoc, isClassic, }; }; @@ -164,21 +168,22 @@ export const useCloudPlanNavigate = ( if (cloudMode !== CloudPlan.Dedicated) { if (cloudModeFromQuery !== cloudMode) { searchParams.set(CLOUD_MODE_KEY, cloudMode); - navigate( - `${pathname}?${searchParams.toString()}${hash || ""}`, - { replace: true } - ); + navigate(`${pathname}?${searchParams.toString()}${hash || ""}`, { + replace: true, + }); } return; } // Dedicated: keep URL without plan param by default; only normalize when an invalid/mismatched plan is present. - if (cloudModeFromQueryRaw && cloudModeFromQueryRaw !== CloudPlan.Dedicated) { + if ( + cloudModeFromQueryRaw && + cloudModeFromQueryRaw !== CloudPlan.Dedicated + ) { searchParams.set(CLOUD_MODE_KEY, CloudPlan.Dedicated); - navigate( - `${pathname}?${searchParams.toString()}${hash || ""}`, - { replace: true } - ); + navigate(`${pathname}?${searchParams.toString()}${hash || ""}`, { + replace: true, + }); } }, [ namespace, diff --git a/src/templates/DocTemplate.tsx b/src/templates/DocTemplate.tsx index c75b16d4..4333f578 100644 --- a/src/templates/DocTemplate.tsx +++ b/src/templates/DocTemplate.tsx @@ -77,6 +77,9 @@ interface DocTemplateProps { essentialNavigation?: { essentialNavigation: RepoNav; }; + byocNavigation?: { + byocNavigation: RepoNav; + }; }; } @@ -119,9 +122,11 @@ function DocTemplate({ navigation: originNav, starterNavigation: starterNav, essentialNavigation: essentialNav, + byocNavigation: byocNav, } = data; - const { cloudPlan, setCloudPlan, isStarter, isEssential } = useCloudPlan(); + const { cloudPlan, setCloudPlan, isStarter, isEssential, isByoc } = + useCloudPlan(); useCloudPlanNavigate( namespace, inDefaultPlan ?? null, @@ -136,6 +141,7 @@ function DocTemplate({ const essentialNavigation = essentialNav ? essentialNav.essentialNavigation : []; + const byocNavigation = byocNav ? byocNav.byocNavigation : []; const navigationByNamespace = namespace !== TOCNamespace.TiDBCloud ? classicNavigation @@ -143,12 +149,15 @@ function DocTemplate({ ? starterNavigation : isEssential ? essentialNavigation + : isByoc + ? byocNavigation : classicNavigation; const navigation = filterTOC(navigationByNamespace); const { language, t } = useI18next(); const haveStarter = starterNavigation.length > 0; const haveEssential = essentialNavigation.length > 0; + const haveByoc = byocNavigation.length > 0; const availablePlans: CloudPlan[] = [CloudPlan.Dedicated]; if (haveStarter) { availablePlans.push(CloudPlan.Starter); @@ -156,6 +165,9 @@ function DocTemplate({ if (haveEssential) { availablePlans.push(CloudPlan.Essential); } + if (haveByoc) { + availablePlans.push(CloudPlan.Byoc); + } const rightTocData: TableOfContent[] | undefined = React.useMemo(() => { let tocItems: TableOfContent[] = []; @@ -409,6 +421,7 @@ export const query = graphql` $navUrl: String! $starterNavUrl: String! $essentialNavUrl: String! + $byocNavUrl: String! ) { site { siteMetadata { @@ -441,6 +454,10 @@ export const query = graphql` essentialNavigation } + byocNavigation: mdx(slug: { eq: $byocNavUrl }) { + byocNavigation + } + locales: allLocale(filter: { language: { eq: $language } }) { edges { node {