From 12404b880d419a8e6d9330a722f1f112dcd4cd51 Mon Sep 17 00:00:00 2001 From: "marie.mcallister" Date: Thu, 16 May 2024 18:50:04 -0700 Subject: [PATCH 01/22] 404 page updates --- pages/404.tsx | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/pages/404.tsx b/pages/404.tsx index 3d2574d95b..b7ce0a93d9 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -4,11 +4,24 @@ import Link from "next/link"; export default function FourOhFour() { return ( - <> -

404 - Page Not Found

- We're very sorry - we could not find the page you were looking for. - Please navigate to the Teleport Documentation or the - Home Page to find what you're looking for. - +
+

404 Page Not Found

+

Sorry, we couldn't find that page

+

We're very sorry - we couldn't find the page you were looking for.

+

Pages you may find useful

+ +
); -} +} \ No newline at end of file From 783949e2801cb482e211ffca9af1edb2199f5169 Mon Sep 17 00:00:00 2001 From: "marie.mcallister" Date: Thu, 16 May 2024 18:59:35 -0700 Subject: [PATCH 02/22] 404 page updates --- pages/404.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/404.tsx b/pages/404.tsx index b7ce0a93d9..db847a3449 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -14,7 +14,7 @@ export default function FourOhFour() {
  • About Us
  • Blog
  • Customer Support
  • -
  • Installation +
  • Installation
  • Installation
  • Teleport Server Access
  • Teleport Kubernetes Access
  • From 5792b27d036a3d4dfb0553023c414a5a7ba646c4 Mon Sep 17 00:00:00 2001 From: "marie.mcallister" Date: Thu, 16 May 2024 19:36:06 -0700 Subject: [PATCH 03/22] 404 page updates --- pages/404.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/404.tsx b/pages/404.tsx index db847a3449..d896726009 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -6,15 +6,15 @@ export default function FourOhFour() { return (

    404 Page Not Found

    -

    Sorry, we couldn't find that page

    -

    We're very sorry - we couldn't find the page you were looking for.

    +

    Sorry, we couldn't find that page

    +

    We couldn't find the page you were looking for.

    Pages you may find useful

    • Home Page
    • About Us
    • Blog
    • Customer Support
    • -
    • Installation
    • +
    • Documentation
    • Installation
    • Teleport Server Access
    • Teleport Kubernetes Access
    • @@ -24,4 +24,4 @@ export default function FourOhFour() {
    ); -} \ No newline at end of file +} From e47dd7bf1a1d5deac7b78c253e19d7d6791ef3f1 Mon Sep 17 00:00:00 2001 From: "marie.mcallister" Date: Thu, 16 May 2024 20:57:54 -0700 Subject: [PATCH 04/22] 404 page updates --- pages/404.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pages/404.tsx b/pages/404.tsx index d896726009..06d71824c8 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -11,16 +11,16 @@ export default function FourOhFour() {

    Pages you may find useful

    • Home Page
    • -
    • About Us
    • -
    • Blog
    • -
    • Customer Support
    • -
    • Documentation
    • -
    • Installation
    • -
    • Teleport Server Access
    • -
    • Teleport Kubernetes Access
    • -
    • Teleport Database Access
    • -
    • Teleport Desktop Access
    • -
    • Teleport Application Access
    • +
    • About Us
    • +
    • Blog
    • +
    • Customer Support
    • +
    • Documentation
    • +
    • Installation
    • +
    • Teleport Server Access
    • +
    • Teleport Kubernetes Access
    • +
    • Teleport Database Access
    • +
    • Teleport Desktop Access
    • +
    • Teleport Application Access
    ); From 6c9d2c9b45349b744c97754308cd7c5525e209fa Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Mon, 3 Jun 2024 17:11:08 -0400 Subject: [PATCH 05/22] Add style guidance for using components (#467) Closes gravitational/teleport#15061 We want to encourage a more textual approach to the docs. Add this as a general goal to the style guide. If ther eare specific violations of this style suggestion that we want to address with documentation efforts, linters, etc., we can plan these separately. --- docs-contributors/style-guide.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs-contributors/style-guide.md b/docs-contributors/style-guide.md index c79a5cb7bf..84b5e1b176 100644 --- a/docs-contributors/style-guide.md +++ b/docs-contributors/style-guide.md @@ -183,6 +183,20 @@ If a commonly debated style question does not have a resolution in this guide (e.g., the Oxford comma), all we ask is that you keep your style consistent within a particular page to maintain a professional polish. +### Use of frontend components + +In general, we want pages in the documentation to emphasize text and provide an +uncluttered experience to readers. Before adding a component besides a +paragraph, heading, or example code snippet, ask what benefit the component adds +to a page, and if it is possible to achieve a similar result with only +paragraphs, headings, and code snippets. + +For example, when adding a `Tabs` component, ask if it would make sense to add a +subheading instead of each `TabItem`. `TabItems` would be useful if only one +variation of the instructions you are adding is relevant to a reader, and the +other two would only add distraction. If all variations of the instructions are +useful, subheadings would make more sense. + ### Voice The documentation should be technically precise and directed toward a technical From 806fa328210adc05b8b12cb85c454835760c4922 Mon Sep 17 00:00:00 2001 From: "marie.mcallister" Date: Mon, 3 Jun 2024 18:54:47 -0700 Subject: [PATCH 06/22] v13 deprecation --- .gitmodules | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index 5111f342d8..8044a68a56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "content/13.x"] - path = content/13.x - url = https://github.com/gravitational/teleport - branch = branch/v13 [submodule "content/14.x"] path = content/14.x url = https://github.com/gravitational/teleport From ccc801ca3edf2e033ec151a0e137cc31c262c3fa Mon Sep 17 00:00:00 2001 From: "marie.mcallister" Date: Mon, 3 Jun 2024 19:02:43 -0700 Subject: [PATCH 07/22] v13 deprecation --- config.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index d2a46fe7d6..4d12d9f75b 100644 --- a/config.json +++ b/config.json @@ -57,7 +57,8 @@ }, { "name": "13.x", - "branch": "branch/v13" + "branch": "branch/v13", + "deprecated": true }, { "name": "14.x", From 8a0c853522c08b02ea780378f21d554ff7739427 Mon Sep 17 00:00:00 2001 From: "marie.mcallister" Date: Mon, 3 Jun 2024 20:24:52 -0700 Subject: [PATCH 08/22] v13 deprecation --- content/13.x | 1 - 1 file changed, 1 deletion(-) delete mode 160000 content/13.x diff --git a/content/13.x b/content/13.x deleted file mode 160000 index 96cbdac87b..0000000000 --- a/content/13.x +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 96cbdac87b73e9944072d4dc227f5480b1b66230 From d47f906925383d9e621662639376fd76d5a98c35 Mon Sep 17 00:00:00 2001 From: Steven Martin Date: Tue, 11 Jun 2024 15:02:57 -0400 Subject: [PATCH 09/22] added javascript to handle version links (#470) * added javascript to handle version links * lint fix --------- Co-authored-by: Steven Martin --- layouts/DocsPage/DocsPage.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/layouts/DocsPage/DocsPage.tsx b/layouts/DocsPage/DocsPage.tsx index c4e7f2b5d5..3ced583faf 100644 --- a/layouts/DocsPage/DocsPage.tsx +++ b/layouts/DocsPage/DocsPage.tsx @@ -67,6 +67,10 @@ const DocsPage = ({ let path = getPath(latest); + let urlCurrent = "/docs" + path; + // handles the case where it's the home page with / to avoid /docs/docs/ + if (path == "/") urlCurrent = "/"; + return ( <> This chapter covers a past release: {current}. We - recommend the latest{" "} + recommend the latest{" "} version instead. )} {isBetaVersion && ( <> This chapter covers an upcoming release: {current}. We - recommend the latest{" "} + recommend the latest{" "} version instead. )} From d84778a14a52dbb41ec6bbbab777502ce4967917 Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Wed, 12 Jun 2024 14:02:51 -0400 Subject: [PATCH 10/22] Update docs config for v16 (#472) - Update config.json - Add a submodule for v17 - Update .gitmodules --- .gitmodules | 4 ++++ config.json | 8 ++++++-- content/17.x | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) create mode 160000 content/17.x diff --git a/.gitmodules b/.gitmodules index 8044a68a56..a4e47f19f9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,4 +9,8 @@ [submodule "content/16.x"] path = content/16.x url = https://github.com/gravitational/teleport + branch = branch/v16 +[submodule "content/17.x"] + path = content/17.x + url = https://github.com/gravitational/teleport branch = master diff --git a/config.json b/config.json index 4d12d9f75b..3c97a47a4a 100644 --- a/config.json +++ b/config.json @@ -66,11 +66,15 @@ }, { "name": "15.x", - "branch": "branch/v15", - "latest": true + "branch": "branch/v15" }, { "name": "16.x", + "branch": "branch/v16", + "latest": true + }, + { + "name": "17.x", "branch": "master" } ] diff --git a/content/17.x b/content/17.x new file mode 160000 index 0000000000..a98b0430ad --- /dev/null +++ b/content/17.x @@ -0,0 +1 @@ +Subproject commit a98b0430add9e1cfdff68acf90960aef396d5822 From d26c52c9fa4c3cae8dffcd5430becc3554d30bd6 Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Wed, 12 Jun 2024 14:36:33 -0400 Subject: [PATCH 11/22] Revert "Update docs config for v16" (#473) This reverts commit fa4bffff2fe0bcb232a9c389788efcc07f4a5117. --- .gitmodules | 4 ---- config.json | 8 ++------ content/17.x | 1 - 3 files changed, 2 insertions(+), 11 deletions(-) delete mode 160000 content/17.x diff --git a/.gitmodules b/.gitmodules index a4e47f19f9..8044a68a56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,8 +9,4 @@ [submodule "content/16.x"] path = content/16.x url = https://github.com/gravitational/teleport - branch = branch/v16 -[submodule "content/17.x"] - path = content/17.x - url = https://github.com/gravitational/teleport branch = master diff --git a/config.json b/config.json index 3c97a47a4a..4d12d9f75b 100644 --- a/config.json +++ b/config.json @@ -66,15 +66,11 @@ }, { "name": "15.x", - "branch": "branch/v15" - }, - { - "name": "16.x", - "branch": "branch/v16", + "branch": "branch/v15", "latest": true }, { - "name": "17.x", + "name": "16.x", "branch": "master" } ] diff --git a/content/17.x b/content/17.x deleted file mode 160000 index a98b0430ad..0000000000 --- a/content/17.x +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a98b0430add9e1cfdff68acf90960aef396d5822 From 56be01949ab749c0e485f023ce34abe593fa3b0f Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Thu, 13 Jun 2024 14:52:16 -0400 Subject: [PATCH 12/22] Revert "Revert "Update docs config for v16" (#473)" (#474) This reverts commit 97bc2e2d8d0cb1b71b69d578a81329717fb0015f. --- .gitmodules | 4 ++++ config.json | 8 ++++++-- content/17.x | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) create mode 160000 content/17.x diff --git a/.gitmodules b/.gitmodules index 8044a68a56..a4e47f19f9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,4 +9,8 @@ [submodule "content/16.x"] path = content/16.x url = https://github.com/gravitational/teleport + branch = branch/v16 +[submodule "content/17.x"] + path = content/17.x + url = https://github.com/gravitational/teleport branch = master diff --git a/config.json b/config.json index 4d12d9f75b..3c97a47a4a 100644 --- a/config.json +++ b/config.json @@ -66,11 +66,15 @@ }, { "name": "15.x", - "branch": "branch/v15", - "latest": true + "branch": "branch/v15" }, { "name": "16.x", + "branch": "branch/v16", + "latest": true + }, + { + "name": "17.x", "branch": "master" } ] diff --git a/content/17.x b/content/17.x new file mode 160000 index 0000000000..a98b0430ad --- /dev/null +++ b/content/17.x @@ -0,0 +1 @@ +Subproject commit a98b0430add9e1cfdff68acf90960aef396d5822 From 4397bef31a2036f963edc91ff2c26f405cb871df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:57:10 -0500 Subject: [PATCH 13/22] Bump braces from 3.0.2 to 3.0.3 (#476) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index cb7a056140..120db1739e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7710,11 +7710,11 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" @@ -10511,10 +10511,10 @@ filesize@^10.0.8: resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.0.tgz#846f5cd8d16e073c5d6767651a8264f6149183cd" integrity sha512-GTLKYyBSDz3nPhlLVPjPWZCnhkd9TrrRArNcy8Z+J2cqScB7h2McAzR6NBX6nYOoWafql0roY8hrocxnZBv9CQ== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" From b5442e1a20eb3530f4e52534d374ce4715acd8c3 Mon Sep 17 00:00:00 2001 From: TuukkaIkius Date: Tue, 18 Jun 2024 15:55:53 +0300 Subject: [PATCH 14/22] feat: add reddit pixel tracking --- pages/_app.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pages/_app.tsx b/pages/_app.tsx index 685a67aa3b..2bf0bbb491 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -55,6 +55,7 @@ export const lato = localLato({ import "styles/varaibles.css"; import "styles/global.css"; +const NEXT_PUBLIC_REDDIT_ID = process.env.NEXT_PUBLIC_REDDIT_ID; const NEXT_PUBLIC_GTM_ID = process.env.NEXT_PUBLIC_GTM_ID; const NEXT_PUBLIC_GTAG_ID = process.env.NEXT_PUBLIC_GTAG_ID; const MUNCHKIN_ID = process.env.MUNCHKIN_ID; @@ -162,6 +163,17 @@ const Analytics = () => { {/* End Google Tag Manager (noscript) */} )} + + {NEXT_PUBLIC_REDDIT_ID && ( + <> + {/* Reddit Pixel */} + + {/* DO NOT MODIFY UNLESS TO REPLACE A USER IDENTIFIER /*} + {/* End Reddit Pixel */} + + )} ); }; From addd800de07b08f0570a7b0372bf7c2f71873aa3 Mon Sep 17 00:00:00 2001 From: hugoShaka Date: Thu, 27 Jun 2024 16:40:15 -0400 Subject: [PATCH 15/22] Allow udnerscore in docs for the Terraform reference --- .remarkrc.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/.remarkrc.mjs b/.remarkrc.mjs index e93ec547cd..abb8cb0be6 100644 --- a/.remarkrc.mjs +++ b/.remarkrc.mjs @@ -48,6 +48,7 @@ const configLint = { ["lint-ordered-list-marker-value", "single"], ["lint-maximum-heading-length", false], ["lint-no-shortcut-reference-link", false], + ["lint-no-file-name-irregular-characters", false], [ remarkIncludes, // Lints (!include.ext!) syntax { From 07060d7d35e9389b2d6bd911b61ee800d743736b Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Tue, 2 Jul 2024 08:44:00 -0400 Subject: [PATCH 16/22] Generate docs sidebar sections automatically (#480) Add a `config.json` field within each `navigation` entry called `generateFrom`. This designates a relative path from `docs/pages` from which to generate entries within the sidebar. When generating pages, a function called `generateNavPaths` looks for pages to use as second-level section introductions. As with the Docusaurus convention, second-level section introductions have the same name as their parent directory. Adding a `generateFrom` field is consistent with the Docusaurus approach to sidebar generation, in which a configuration field indicates which directory to generate a section from. This gives us control over the title and icons we use for navigation sections, which aren't available to fetch from a directory tree alone. It also lets us use the current, hardcoded `entries` approach for some sections if we need to. Also un-skips some accidentally skipped tests. --- layouts/DocsPage/types.ts | 1 + server/config-docs.ts | 28 ++++-- .../fixtures/result/code-snippet-heredoc.mdx | 2 + server/pages-helpers.ts | 77 +++++++++++++++- uvu-tests/config-docs.test.ts | 89 ++++++++++++++++++- uvu-tests/remark-code-snippet.test.ts | 2 +- uvu-tests/remark-includes.test.ts | 23 +++-- 7 files changed, 199 insertions(+), 23 deletions(-) diff --git a/layouts/DocsPage/types.ts b/layouts/DocsPage/types.ts index ccf33daf48..be29a372e1 100644 --- a/layouts/DocsPage/types.ts +++ b/layouts/DocsPage/types.ts @@ -43,6 +43,7 @@ export interface NavigationCategory { icon: IconName; title: string; entries: NavigationItem[]; + generateFrom?: string; } interface LinkWithRedirect { diff --git a/server/config-docs.ts b/server/config-docs.ts index 9ef49c4608..83aea430bd 100644 --- a/server/config-docs.ts +++ b/server/config-docs.ts @@ -8,11 +8,12 @@ import type { Redirect } from "next/dist/lib/load-custom-routes"; import Ajv from "ajv"; import { validateConfig } from "./config-common"; -import { resolve, join } from "path"; -import { existsSync, readFileSync } from "fs"; +import { dirname, resolve, join } from "path"; +import fs from "fs"; import { isExternalLink, isHash, splitPath } from "../utils/url"; import { NavigationCategory, NavigationItem } from "../layouts/DocsPage/types"; import { loadConfig as loadSiteConfig } from "./config-site"; +import { generateNavPaths } from "./pages-helpers"; const { latest } = loadSiteConfig(); export interface Config { @@ -31,8 +32,8 @@ const getConfigPath = (version: string) => export const load = (version: string) => { const path = getConfigPath(version); - if (existsSync(path)) { - const content = readFileSync(path, "utf-8"); + if (fs.existsSync(path)) { + const content = fs.readFileSync(path, "utf-8"); return JSON.parse(content) as Config; } else { @@ -61,6 +62,8 @@ const validator = ajv.compile({ properties: { icon: { type: "string" }, title: { type: "string" }, + generateFrom: { type: "string" }, + // Entries must be empty if generateFrom is present. entries: { type: "array", items: { @@ -228,7 +231,7 @@ const correspondingFileExistsForURL = ( if ( [docsPagePath, indexPath, introPath].find((p) => { - return existsSync(p); + return fs.existsSync(p); }) == undefined ) { return false; @@ -304,8 +307,6 @@ export const normalize = (config: Config, version: string): Config => { return config; }; -/* Load, validate and normalize config. */ - export const loadConfig = (version: string) => { const config = load(version); @@ -327,5 +328,18 @@ export const loadConfig = (version: string) => { validateConfig(validator, config); + config.navigation.forEach((item, i) => { + if (!!item.generateFrom && item.entries.length > 0) { + throw "a navigation item cannot contain both generateFrom and entries"; + } + + if (!!item.generateFrom) { + config.navigation[i].entries = generateNavPaths( + fs, + join("content", version, "docs", "pages", item.generateFrom) + ); + } + }); + return normalize(config, version); }; diff --git a/server/fixtures/result/code-snippet-heredoc.mdx b/server/fixtures/result/code-snippet-heredoc.mdx index 755973932f..a7a9a41bff 100644 --- a/server/fixtures/result/code-snippet-heredoc.mdx +++ b/server/fixtures/result/code-snippet-heredoc.mdx @@ -67,6 +67,8 @@ +
    + Create role diff --git a/server/pages-helpers.ts b/server/pages-helpers.ts index 00b652b4af..b755648f09 100644 --- a/server/pages-helpers.ts +++ b/server/pages-helpers.ts @@ -4,9 +4,9 @@ import type { MDXPage, MDXPageData, MDXPageFrontmatter } from "./types-unist"; -import { resolve } from "path"; import { readSync } from "to-vfile"; import matter from "gray-matter"; +import { sep, parse, dirname, resolve, join } from "path"; export const extensions = ["md", "mdx", "ts", "tsx", "js", "jsx"]; @@ -61,3 +61,78 @@ export const getPageInfo = ( return result; }; + +const getEntryForPath = (fs, filePath) => { + const txt = fs.readFileSync(filePath, "utf8"); + const { data } = matter(txt); + const slug = filePath.split("docs/pages")[1].replace(".mdx", "/"); + return { + title: data.title, + slug: slug, + }; +}; + +export const generateNavPaths = (fs, dirPath) => { + const firstLvl = fs.readdirSync(dirPath, "utf8"); + let result = []; + let firstLvlFiles = new Set(); + let firstLvlDirs = new Set(); + firstLvl.forEach((p) => { + const fullPath = join(dirPath, p); + const info = fs.statSync(fullPath); + if (info.isDirectory()) { + firstLvlDirs.add(fullPath); + return; + } + firstLvlFiles.add(fullPath); + }); + let sectionIntros = new Set(); + firstLvlDirs.forEach((d: string) => { + const { name } = parse(d); + const asFile = join(d, name + ".mdx"); + + if (!fs.existsSync(asFile)) { + throw `subdirectory in generated sidebar section ${d} has no category page ${asFile}`; + } + sectionIntros.add(asFile); + return; + }); + + // Add files with no corresponding directory to the navigation first. Section + // introductions, by convention, have a filename that corresponds to the + // subdirectory containing pages in the section, or have the name + // "introduction.mdx". + firstLvlFiles.forEach((f) => { + result.push(getEntryForPath(fs, f)); + }); + + sectionIntros.forEach((si: string) => { + const { slug, title } = getEntryForPath(fs, si); + const section = { + title: title, + slug: slug, + entries: [], + }; + const sectionDir = dirname(si); + const secondLvl = fs.readdirSync(sectionDir, "utf8"); + secondLvl.forEach((f2) => { + const { name } = parse(f2); + + // The directory name is the same as the filename, meaning that we have + // already used this as a category page. + if (sectionDir.endsWith(name)) { + return; + } + + const fullPath2 = join(sectionDir, f2); + const stat = fs.statSync(fullPath2); + if (stat.isDirectory()) { + return; + } + + section.entries.push(getEntryForPath(fs, fullPath2)); + }); + result.push(section); + }); + return result; +}; diff --git a/uvu-tests/config-docs.test.ts b/uvu-tests/config-docs.test.ts index 6fda3675ac..7db74aa009 100644 --- a/uvu-tests/config-docs.test.ts +++ b/uvu-tests/config-docs.test.ts @@ -2,9 +2,10 @@ import { Redirect } from "next/dist/lib/load-custom-routes"; import { suite } from "uvu"; import * as assert from "uvu/assert"; import { Config, checkURLsForCorrespondingFiles } from "../server/config-docs"; +import { generateNavPaths } from "../server/pages-helpers"; import { randomUUID } from "crypto"; import { join } from "path"; -import { opendirSync } from "fs"; +import { Volume, createFsFromVolume } from "memfs"; const Suite = suite("server/config-docs"); @@ -112,4 +113,90 @@ Suite("Ensures that URLs correspond to docs pages", () => { assert.equal(actual, expected); }); +Suite("generateNavPaths generates a sidebar from a file tree", () => { + const files = { + "/docs/pages/database-access/introduction.mdx": `--- +title: Protect Databases with Teleport +---`, + "/docs/pages/database-access/guides/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/postgres.mdx": `--- +title: Postgres Guide +---`, + "/docs/pages/database-access/guides/mysql.mdx": `--- +title: MySQL Guide +---`, + "/docs/pages/database-access/rbac/rbac.mdx": `--- +title: Database Access RBAC +---`, + "/docs/pages/database-access/rbac/get-started.mdx": `--- +title: Get Started with DB RBAC +---`, + "/docs/pages/database-access/rbac/reference.mdx": `--- +title: Database RBAC Reference +---`, + }; + + const expected = [ + { + title: "Protect Databases with Teleport", + slug: "/database-access/introduction/", + }, + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + { + title: "Database Access RBAC", + slug: "/database-access/rbac/rbac/", + entries: [ + { + title: "Get Started with DB RBAC", + slug: "/database-access/rbac/get-started/", + }, + { + title: "Database RBAC Reference", + slug: "/database-access/rbac/reference/", + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); +}); + +Suite( + "generateNavPaths throws if there is no category page in a subdirectory", + () => { + const files = { + "/docs/pages/database-access/guides/postgres.mdx": `--- +title: Postgres Guide +---`, + "/docs/pages/database-access/guides/mysql.mdx": `--- +title: MySQL Guide +---`, + }; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + assert.throws(() => { + generateNavPaths(fs, "/docs/pages/database-access"); + }, "database-access/guides/guides.mdx"); + } +); + Suite.run(); diff --git a/uvu-tests/remark-code-snippet.test.ts b/uvu-tests/remark-code-snippet.test.ts index d04ee351c0..1196104863 100644 --- a/uvu-tests/remark-code-snippet.test.ts +++ b/uvu-tests/remark-code-snippet.test.ts @@ -243,7 +243,7 @@ Suite("Variables in multiline command support", () => { assert.equal(result, expected); }); -Suite.only("Includes empty lines in example command output", () => { +Suite("Includes empty lines in example command output", () => { const value = readFileSync( resolve("server/fixtures/code-snippet-empty-line.mdx"), "utf-8" diff --git a/uvu-tests/remark-includes.test.ts b/uvu-tests/remark-includes.test.ts index 1a68a8eae2..ea3f651df5 100644 --- a/uvu-tests/remark-includes.test.ts +++ b/uvu-tests/remark-includes.test.ts @@ -590,21 +590,19 @@ boundary" section. } ); -Suite.only( - "Interprets anchor-only links correctly when loading partials", - () => { - const actual = transformer({ - value: `Here is the outer page. +Suite("Interprets anchor-only links correctly when loading partials", () => { + const actual = transformer({ + value: `Here is the outer page. (!anchor-links.mdx!) `, - path: "server/fixtures/mypage.mdx", - }).toString(); + path: "server/fixtures/mypage.mdx", + }).toString(); - assert.equal( - actual, - `Here is the outer page. + assert.equal( + actual, + `Here is the outer page. This is a [link to an anchor](#this-is-a-section). @@ -612,8 +610,7 @@ This is a [link to an anchor](#this-is-a-section). This is content within the section. ` - ); - } -); + ); +}); Suite.run(); From bb54834a7848e114e7e3b80e7b7cf73f28e26d01 Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Wed, 3 Jul 2024 12:55:44 -0400 Subject: [PATCH 17/22] Improve the sidebar generator (#481) Improve the sidebar generator to accommodate the reorganized docs site. - Alphabetize auto-generated sidebar entries. The exception is any page that includes "Introduction" or "introduction" in the title. Elevate these to the first entry to avoid a confusing sidebar order. - Show third-level category pages in the sidebar without showing their contents. This way, we can add content one level beyond the level permitted in the sidebar while still showing the category page for that content in the sidebar. - Allow for a category page to be at the same level as its associated subdirectory. This is to accommodate the Terraform Provider reference which, because of the way it's generated, can only include a category page outside of its associated subdirectory. This does not abide by the Docusaurus convention, so we will need to figure out a solution eventually, but this is a quick alternative to tide us over. - Only add ".mdx" files to the sidebar. Otherwise, the generator attempts to add vim swapfiles etc. --- server/pages-helpers.ts | 108 ++++++++++++++---- uvu-tests/config-docs.test.ts | 204 ++++++++++++++++++++++++++++++++-- 2 files changed, 281 insertions(+), 31 deletions(-) diff --git a/server/pages-helpers.ts b/server/pages-helpers.ts index b755648f09..4dfcb43e20 100644 --- a/server/pages-helpers.ts +++ b/server/pages-helpers.ts @@ -72,6 +72,42 @@ const getEntryForPath = (fs, filePath) => { }; }; +// sortByTitle takes two navigation entries, a and b, and sorts them in +// alphabetically ascending order by their "title" field. If either title +// includes the substring "introduction", sortByTitle sorts that entry first. +const sortByTitle = (a, b) => { + switch (true) { + case a.title.toLowerCase().includes("introduction"): + return -1; + break; + case b.title.toLowerCase().includes("introduction"): + return 1; + break; + default: + return a.title < b.title ? -1 : 1; + } +}; + +// categoryPagePathForDir looks for a category page at the same directory level +// as its associated directory OR within the associated directory. Throws an +// error if there is no category page for the directory. +const categoryPagePathForDir = (fs, dirPath) => { + const { name } = parse(dirPath); + + const outerCategoryPage = join(dirname(dirPath), name + ".mdx"); + const innerCategoryPage = join(dirPath, name + ".mdx"); + + if (fs.existsSync(outerCategoryPage)) { + return outerCategoryPage; + } + if (fs.existsSync(innerCategoryPage)) { + return innerCategoryPage; + } + throw new Error( + `subdirectory in generated sidebar section ${dirPath} has no category page ${innerCategoryPage} or ${outerCategoryPage}` + ); +}; + export const generateNavPaths = (fs, dirPath) => { const firstLvl = fs.readdirSync(dirPath, "utf8"); let result = []; @@ -86,53 +122,79 @@ export const generateNavPaths = (fs, dirPath) => { } firstLvlFiles.add(fullPath); }); - let sectionIntros = new Set(); - firstLvlDirs.forEach((d: string) => { - const { name } = parse(d); - const asFile = join(d, name + ".mdx"); - if (!fs.existsSync(asFile)) { - throw `subdirectory in generated sidebar section ${d} has no category page ${asFile}`; - } - sectionIntros.add(asFile); - return; + // Map category pages to the directories they introduce so we can can add a + // sidebar entry for the category page, then traverse the directory. + let sectionIntros = new Map(); + firstLvlDirs.forEach((d: string) => { + sectionIntros.set(categoryPagePathForDir(fs, d), d); }); // Add files with no corresponding directory to the navigation first. Section // introductions, by convention, have a filename that corresponds to the // subdirectory containing pages in the section, or have the name // "introduction.mdx". - firstLvlFiles.forEach((f) => { + firstLvlFiles.forEach((f: string) => { + // Handle section intros separately + if (sectionIntros.has(f)) { + return; + } + if (!f.endsWith(".mdx")) { + return; + } result.push(getEntryForPath(fs, f)); }); - sectionIntros.forEach((si: string) => { - const { slug, title } = getEntryForPath(fs, si); + sectionIntros.forEach((dirPath, categoryPagePath) => { + const { slug, title } = getEntryForPath(fs, categoryPagePath); const section = { title: title, slug: slug, entries: [], }; - const sectionDir = dirname(si); - const secondLvl = fs.readdirSync(sectionDir, "utf8"); - secondLvl.forEach((f2) => { - const { name } = parse(f2); - - // The directory name is the same as the filename, meaning that we have - // already used this as a category page. - if (sectionDir.endsWith(name)) { + const secondLvl = new Set(fs.readdirSync(dirPath, "utf8")); + + // Find all second-level category pages first so we don't + // repeat them in the sidebar. + secondLvl.forEach((f2: string) => { + let fullPath2 = join(dirPath, f2); + const stat = fs.statSync(fullPath2); + + // List category pages on the second level, but not their contents. + if (!stat.isDirectory()) { return; } + const catPath = categoryPagePathForDir(fs, fullPath2); + fullPath2 = catPath; + secondLvl.delete(f2); - const fullPath2 = join(sectionDir, f2); - const stat = fs.statSync(fullPath2); - if (stat.isDirectory()) { + // Delete the category page from the set so we don't add it again + // when we add individual files. + secondLvl.delete(parse(catPath).base); + section.entries.push(getEntryForPath(fs, fullPath2)); + }); + + secondLvl.forEach((f2: string) => { + // Only add entries for MDX files here + if (!f2.endsWith(".mdx")) { return; } + let fullPath2 = join(dirPath, f2); + + // This is a first-level category page that happens to exist on the second + // level. + if (sectionIntros.has(fullPath2)) { + return; + } + + const stat = fs.statSync(fullPath2); section.entries.push(getEntryForPath(fs, fullPath2)); }); + + section.entries.sort(sortByTitle); result.push(section); }); + result.sort(sortByTitle); return result; }; diff --git a/uvu-tests/config-docs.test.ts b/uvu-tests/config-docs.test.ts index 7db74aa009..bdef7dc9a4 100644 --- a/uvu-tests/config-docs.test.ts +++ b/uvu-tests/config-docs.test.ts @@ -139,10 +139,6 @@ title: Database RBAC Reference }; const expected = [ - { - title: "Protect Databases with Teleport", - slug: "/database-access/introduction/", - }, { title: "Database Access Guides", slug: "/database-access/guides/guides/", @@ -161,16 +157,20 @@ title: Database RBAC Reference title: "Database Access RBAC", slug: "/database-access/rbac/rbac/", entries: [ - { - title: "Get Started with DB RBAC", - slug: "/database-access/rbac/get-started/", - }, { title: "Database RBAC Reference", slug: "/database-access/rbac/reference/", }, + { + title: "Get Started with DB RBAC", + slug: "/database-access/rbac/get-started/", + }, ], }, + { + title: "Protect Databases with Teleport", + slug: "/database-access/introduction/", + }, ]; const vol = Volume.fromJSON(files); @@ -179,6 +179,96 @@ title: Database RBAC Reference assert.equal(actual, expected); }); +Suite( + "generateNavPaths alphabetizes second-level links except 'Introduction'", + () => { + const files = { + "/docs/pages/database-access/mongodb.mdx": `--- +title: MongoDB +---`, + "/docs/pages/database-access/azure-dbs.mdx": `--- +title: Azure +---`, + "/docs/pages/database-access/introduction.mdx": `--- +title: Introduction to Database Access +---`, + }; + + const expected = [ + { + title: "Introduction to Database Access", + slug: "/database-access/introduction/", + }, + { + title: "Azure", + slug: "/database-access/azure-dbs/", + }, + { + title: "MongoDB", + slug: "/database-access/mongodb/", + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); + } +); + +Suite( + "generateNavPaths alphabetizes third-level links except 'Introduction'", + () => { + const files = { + "/docs/pages/database-access/guides/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/postgres.mdx": `--- +title: Postgres Guide +---`, + "/docs/pages/database-access/guides/mysql.mdx": `--- +title: MySQL Guide +---`, + "/docs/pages/database-access/guides/get-started.mdx": `--- +title: Introduction to Database RBAC +---`, + "/docs/pages/database-access/guides/reference.mdx": `--- +title: Database RBAC Reference +---`, + }; + + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "Introduction to Database RBAC", + slug: "/database-access/guides/get-started/", + }, + { + title: "Database RBAC Reference", + slug: "/database-access/guides/reference/", + }, + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); + } +); + Suite( "generateNavPaths throws if there is no category page in a subdirectory", () => { @@ -199,4 +289,102 @@ title: MySQL Guide } ); +Suite( + "generateNavPaths shows third-level category pages on the sidebar", + () => { + const files = { + "/docs/pages/database-access/guides/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/postgres.mdx": `--- +title: Postgres Guide +---`, + "/docs/pages/database-access/guides/mysql.mdx": `--- +title: MySQL Guide +---`, + "/docs/pages/database-access/guides/rbac/rbac.mdx": `--- +title: Database Access RBAC +---`, + "/docs/pages/database-access/guides/rbac/get-started.mdx": `--- +title: Get Started with DB RBAC +---`, + }; + + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "Database Access RBAC", + slug: "/database-access/guides/rbac/rbac/", + }, + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); + } +); + +Suite( + "allows category pages in the same directory as the associated subdirectory", + () => { + const files = { + "/docs/pages/database-access/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/postgres.mdx": `--- +title: Postgres Guide +---`, + "/docs/pages/database-access/guides/mysql.mdx": `--- +title: MySQL Guide +---`, + "/docs/pages/database-access/guides/rbac.mdx": `--- +title: Database Access RBAC +---`, + "/docs/pages/database-access/guides/rbac/get-started.mdx": `--- +title: Get Started with DB RBAC +---`, + }; + + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/", + entries: [ + { + title: "Database Access RBAC", + slug: "/database-access/guides/rbac/", + }, + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + let actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); + } +); + Suite.run(); From 238de6805197cdb69dd119e95dcc6ab51b868f96 Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Tue, 9 Jul 2024 12:41:55 -0400 Subject: [PATCH 18/22] Add a remark plugin for generating a TOC menu (#483) As we reorganize the documentation, it becomes cumbersome to manually change lists of links to pages within the documentation, e.g., in table of contents pages for subsections of the docs. This change adds a remark plugin for generating a list of links to pages in the current directory. The plugin works similarly to `remark-includes`, and accesses the local filesystem during the docs build. It replaces any lines consisting of `(!toc!)` with a list of links to pages in the current directory. The assumption is that a category page within a directory can use this to list contents. --- .remarkrc.mjs | 2 + docs-contributors/ui-reference.md | 17 ++ .../toc/database-access/database-access.mdx | 4 + server/fixtures/toc/database-access/mysql.mdx | 4 + .../fixtures/toc/database-access/postgres.mdx | 4 + .../fixtures/toc/database-access/source.mdx | 5 + server/fixtures/toc/expected.mdx | 7 + server/markdown-config.ts | 3 + server/remark-toc.ts | 120 ++++++++++++ uvu-tests/remark-toc.test.ts | 185 ++++++++++++++++++ 10 files changed, 351 insertions(+) create mode 100644 server/fixtures/toc/database-access/database-access.mdx create mode 100644 server/fixtures/toc/database-access/mysql.mdx create mode 100644 server/fixtures/toc/database-access/postgres.mdx create mode 100644 server/fixtures/toc/database-access/source.mdx create mode 100644 server/fixtures/toc/expected.mdx create mode 100644 server/remark-toc.ts create mode 100644 uvu-tests/remark-toc.test.ts diff --git a/.remarkrc.mjs b/.remarkrc.mjs index abb8cb0be6..5d798247e0 100644 --- a/.remarkrc.mjs +++ b/.remarkrc.mjs @@ -4,6 +4,7 @@ import remarkIncludes from "./.build/server/remark-includes.mjs"; import remarkCodeSnippet from "./.build/server/remark-code-snippet.mjs"; import remarkLintDetails from "./.build/server/remark-lint-details.mjs"; import remarkLintFrontmatter from "./.build/server/remark-lint-frontmatter.mjs"; +import remarkTOC from "./.build/server/remark-toc.mjs"; import { remarkLintTeleportDocsLinks} from "./.build/server/lint-teleport-docs-links.mjs" import { getVersion, @@ -49,6 +50,7 @@ const configLint = { ["lint-maximum-heading-length", false], ["lint-no-shortcut-reference-link", false], ["lint-no-file-name-irregular-characters", false], + [remarkTOC], [ remarkIncludes, // Lints (!include.ext!) syntax { diff --git a/docs-contributors/ui-reference.md b/docs-contributors/ui-reference.md index 4055cae1c8..5a942e610b 100644 --- a/docs-contributors/ui-reference.md +++ b/docs-contributors/ui-reference.md @@ -338,6 +338,23 @@ Here is an image: When including the partial, the docs engine will rewrite the link path to load the image in `docs/img/screenshot.png`. +## Tables of Contents + +You can add a list of links to pages in the current directory by adding the +following line to a docs page: + +``` +(!toc!) +``` + +The docs engine replaces this line with a list of links to pages in the current +directory, using the title and description of each page to populate the link: + +``` +- [Page 1](page1.mdx): This is a description of Page 1. +- [Page 2](page2.mdx): This is a description of Page 2. +``` + ## Tabs To insert a tabs block like the one above, use this syntax: diff --git a/server/fixtures/toc/database-access/database-access.mdx b/server/fixtures/toc/database-access/database-access.mdx new file mode 100644 index 0000000000..b2d241c9e6 --- /dev/null +++ b/server/fixtures/toc/database-access/database-access.mdx @@ -0,0 +1,4 @@ +--- +title: Protect Databases with Teleport +description: Guides to protecting databases with Teleport. +--- diff --git a/server/fixtures/toc/database-access/mysql.mdx b/server/fixtures/toc/database-access/mysql.mdx new file mode 100644 index 0000000000..d4d3b4602e --- /dev/null +++ b/server/fixtures/toc/database-access/mysql.mdx @@ -0,0 +1,4 @@ +--- +title: Protect MySQL with Teleport +description: How to enroll your MySQL database with Teleport +--- diff --git a/server/fixtures/toc/database-access/postgres.mdx b/server/fixtures/toc/database-access/postgres.mdx new file mode 100644 index 0000000000..5b5c52a58a --- /dev/null +++ b/server/fixtures/toc/database-access/postgres.mdx @@ -0,0 +1,4 @@ +--- +title: Protect Postgres with Teleport +description: How to enroll Postgres with your Teleport cluster +--- diff --git a/server/fixtures/toc/database-access/source.mdx b/server/fixtures/toc/database-access/source.mdx new file mode 100644 index 0000000000..d89f98fd6c --- /dev/null +++ b/server/fixtures/toc/database-access/source.mdx @@ -0,0 +1,5 @@ +## Header + +Here is an intro. + +(!toc!) diff --git a/server/fixtures/toc/expected.mdx b/server/fixtures/toc/expected.mdx new file mode 100644 index 0000000000..9cbec5eee8 --- /dev/null +++ b/server/fixtures/toc/expected.mdx @@ -0,0 +1,7 @@ +## Header + +Here is an intro. + +* [Protect Databases with Teleport](database-access.mdx): Guides to protecting databases with Teleport. +* [Protect MySQL with Teleport](mysql.mdx): How to enroll your MySQL database with Teleport +* [Protect Postgres with Teleport](postgres.mdx): How to enroll Postgres with your Teleport cluster diff --git a/server/markdown-config.ts b/server/markdown-config.ts index 02c71a4eb9..053654667d 100644 --- a/server/markdown-config.ts +++ b/server/markdown-config.ts @@ -16,6 +16,7 @@ import remarkVariables from "./remark-variables"; import remarkCodeSnippet from "./remark-code-snippet"; import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; +import remarkTOC from "./remark-toc"; import remarkCopyLinkedFiles from "remark-copy-linked-files"; import rehypeImages from "./rehype-images"; import { getVersion, getVersionRootPath } from "./docs-helpers"; @@ -42,6 +43,8 @@ export const transformToAST = async (value: string, vfile: VFile) => { // run() will apply plugins and return modified AST const AST = await unified() + // Resolves (!toc dir/path!) syntax + .use(remarkTOC) .use(remarkIncludes, { rootDir: getVersionRootPath(vfile.path), }) // Resolves (!include.ext!) syntax diff --git a/server/remark-toc.ts b/server/remark-toc.ts new file mode 100644 index 0000000000..15bd2c88bd --- /dev/null +++ b/server/remark-toc.ts @@ -0,0 +1,120 @@ +import * as nodeFS from "fs"; +import path from "path"; +import matter from "gray-matter"; +import { visitParents } from "unist-util-visit-parents"; +import { fromMarkdown } from "mdast-util-from-markdown"; +import type { Parent } from "unist"; +import type { VFile } from "vfile"; +import type { Content } from "mdast"; +import type { Transformer } from "unified"; + +// relativePathToFile takes a filepath and returns a path we can use in links +// to the file in a table of contents page. The link path is a relative path +// to the directory where we are placing the table of contents page. +// @param root {string} - the directory path to the table of contents page. +// @param filepath {string} - the path from which to generate a link path. +const relativePathToFile = (root: string, filepath: string) => { + // Return the filepath without the first segment, removing the first + // slash. This is because the TOC file we are generating is located at + // root. + return filepath.slice(root.length).replace(/^\//, ""); +}; + +// getTOC generates a list of links to all files in the same directory as +// filePath except for filePath. The return value is an object with two +// properties: +// - result: a string containing the resulting list of links. +// - error: an error message encountered during processing +export const getTOC = (filePath: string, fs = nodeFS) => { + const dirPath = path.dirname(filePath); + if (!fs.existsSync(dirPath)) { + return { + error: `Cannot generate a table of contents for nonexistent directory at ${dirPath}`, + }; + } + + const { name } = path.parse(filePath); + + const files = fs.readdirSync(dirPath, "utf8"); + let mdxFiles = new Set(); + const dirs = files.reduce((accum, current) => { + // Don't add a TOC entry for the current file. + if (name == path.parse(current).name) { + return accum; + } + const stats = fs.statSync(path.join(dirPath, current)); + if (!stats.isDirectory() && current.endsWith(".mdx")) { + mdxFiles.add(path.join(dirPath, current)); + return accum; + } + accum.add(path.join(dirPath, current)); + return accum; + }, new Set()); + + // Add rows to the menu page for non-menu pages. + const entries = []; + mdxFiles.forEach((f: string, idx: number) => { + const text = fs.readFileSync(f, "utf8"); + let relPath = relativePathToFile(dirPath, f); + const { data } = matter(text); + entries.push(`- [${data.title}](${relPath}): ${data.description}`); + }); + + // Add rows to the menu page for first-level child menu pages + dirs.forEach((f: string, idx: number) => { + const menuPath = path.join(f, path.parse(f).base + ".mdx"); + if (!fs.existsSync(menuPath)) { + return { + error: `there must be a page called ${menuPath} that introduces ${f}`, + }; + } + const text = fs.readFileSync(menuPath, "utf8"); + let relPath = relativePathToFile(dirPath, menuPath); + const { data } = matter(text); + + entries.push(`- [${data.title}](${relPath}): ${data.description}`); + }); + entries.sort(); + return { result: entries.join("\n") }; +}; + +const tocRegexpPattern = "^\\(!toc!\\)$"; + +// remarkTOC replaces (!toc!) syntax in a page with a list of docs pages at a +// given directory location. +export default function remarkTOC(): Transformer { + return (root: Content, vfile: VFile) => { + const lastErrorIndex = vfile.messages.length; + + visitParents(root, (node, ancestors: Parent[]) => { + if (node.type !== "text") { + return; + } + const parent = ancestors[ancestors.length - 1]; + + if (parent.type !== "paragraph") { + return; + } + if (!parent.children || parent.children.length !== 1) { + return; + } + + const tocExpr = node.value.trim().match(tocRegexpPattern); + if (!tocExpr) { + return; + } + + const { result, error } = getTOC(vfile.path); + if (!!error) { + vfile.message(error, node); + return; + } + const tree = fromMarkdown(result, {}); + + const grandParent = ancestors[ancestors.length - 2] as Parent; + const parentIndex = grandParent.children.indexOf(parent); + + grandParent.children.splice(parentIndex, 1, ...tree.children); + }); + }; +} diff --git a/uvu-tests/remark-toc.test.ts b/uvu-tests/remark-toc.test.ts new file mode 100644 index 0000000000..d0390f9be7 --- /dev/null +++ b/uvu-tests/remark-toc.test.ts @@ -0,0 +1,185 @@ +import { Volume, createFsFromVolume } from "memfs"; +import { default as remarkTOC, getTOC } from "../server/remark-toc"; +import { readFileSync } from "fs"; +import { resolve } from "path"; +import { suite } from "uvu"; +import * as assert from "uvu/assert"; +import { VFile, VFileOptions } from "vfile"; +import remarkMdx from "remark-mdx"; +import remarkGFM from "remark-gfm"; +import { remark } from "remark"; + +const Suite = suite("server/remark-toc"); + +const testFilesTwoSections = { + "/docs/docs.mdx": `--- +title: "Documentation Home" +description: "Guides to setting up the product." +--- + +Guides to setting up the product. + +`, + "/docs/database-access/database-access.mdx": `--- +title: "Database Access" +description: Guides related to Database Access. +--- + +Guides related to Database Access. + +`, + "/docs/database-access/page1.mdx": `--- +title: "Database Access Page 1" +description: "Protecting DB 1 with Teleport" +---`, + "/docs/database-access/page2.mdx": `--- +title: "Database Access Page 2" +description: "Protecting DB 2 with Teleport" +---`, + "/docs/application-access/application-access.mdx": `--- +title: "Application Access" +description: "Guides related to Application Access" +--- + +Guides related to Application Access. + +`, + "/docs/application-access/page1.mdx": `--- +title: "Application Access Page 1" +description: "Protecting App 1 with Teleport" +---`, + "/docs/application-access/page2.mdx": `--- +title: "Application Access Page 2" +description: "Protecting App 2 with Teleport" +---`, +}; + +Suite("getTOC with one link to a directory", () => { + const expected = `- [Application Access](application-access/application-access.mdx): Guides related to Application Access`; + + const vol = Volume.fromJSON({ + "/docs/docs.mdx": `--- +title: Documentation Home +description: Guides for setting up the product. +--- + +Guides for setting up the product. + +`, + "/docs/application-access/application-access.mdx": `--- +title: "Application Access" +description: "Guides related to Application Access" +--- + +`, + "/docs/application-access/page1.mdx": `--- +title: "Application Access Page 1" +description: "Protecting App 1 with Teleport" +---`, + "/docs/application-access/page2.mdx": `--- +title: "Application Access Page 2" +description: "Protecting App 2 with Teleport" +---`, + }); + const fs = createFsFromVolume(vol); + const actual = getTOC("/docs/docs.mdx", fs); + assert.equal(actual.result, expected); +}); + +Suite("getTOC with multiple links to directories", () => { + const expected = `- [Application Access](application-access/application-access.mdx): Guides related to Application Access +- [Database Access](database-access/database-access.mdx): Guides related to Database Access.`; + + const vol = Volume.fromJSON(testFilesTwoSections); + const fs = createFsFromVolume(vol); + const actual = getTOC("/docs/docs.mdx", fs); + assert.equal(actual.result, expected); +}); + +Suite("getTOC orders sections correctly", () => { + const expected = `- [API Usage](api.mdx): Using the API. +- [Application Access](application-access/application-access.mdx): Guides related to Application Access +- [Desktop Access](desktop-access/desktop-access.mdx): Guides related to Desktop Access +- [Initial Setup](initial-setup.mdx): How to set up the product for the first time. +- [Kubernetes](kubernetes.mdx): A guide related to Kubernetes.`; + + const vol = Volume.fromJSON({ + "/docs/docs.mdx": `--- +title: Documentation Home +description: Guides to setting up the product. +--- + +Guides to setting up the product. + +`, + "/docs/desktop-access/desktop-access.mdx": `--- +title: "Desktop Access" +description: "Guides related to Desktop Access" +--- + +`, + + "/docs/application-access/application-access.mdx": `--- +title: "Application Access" +description: "Guides related to Application Access" +--- + +`, + "/docs/desktop-access/get-started.mdx": `--- +title: "Get Started" +description: "Get started with desktop access." +---`, + "/docs/application-access/page1.mdx": `--- +title: "Application Access Page 1" +description: "Protecting App 1 with Teleport" +---`, + "/docs/kubernetes.mdx": `--- +title: "Kubernetes" +description: "A guide related to Kubernetes." +---`, + + "/docs/initial-setup.mdx": `--- +title: "Initial Setup" +description: "How to set up the product for the first time." +---`, + "/docs/api.mdx": `--- +title: "API Usage" +description: "Using the API." +---`, + }); + const fs = createFsFromVolume(vol); + const actual = getTOC("/docs/docs.mdx", fs); + assert.equal(actual.result, expected); +}); + +const transformer = (vfileOptions: VFileOptions) => { + const file = new VFile(vfileOptions); + + return remark() + .use(remarkMdx) + .use(remarkGFM) + .use(remarkTOC) + .processSync(file); +}; + +Suite("replaces inclusion expressions", () => { + const sourcePath = "server/fixtures/toc/database-access/source.mdx"; + const value = readFileSync(resolve(sourcePath), "utf-8"); + + const result = transformer({ + value, + path: sourcePath, + }); + + const actual = result.toString(); + + const expected = readFileSync( + resolve("server/fixtures/toc/expected.mdx"), + "utf-8" + ); + + assert.equal(result.messages, []); + assert.equal(actual, expected); +}); + +Suite.run(); From 3e352718cd4d9bf94a0618639af2c1dd26d394ff Mon Sep 17 00:00:00 2001 From: "M.C.M" Date: Tue, 9 Jul 2024 13:45:28 -0700 Subject: [PATCH 19/22] Update style-guide.md Updating description requirements --- docs-contributors/style-guide.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs-contributors/style-guide.md b/docs-contributors/style-guide.md index 84b5e1b176..f3407d5d4c 100644 --- a/docs-contributors/style-guide.md +++ b/docs-contributors/style-guide.md @@ -60,7 +60,7 @@ In most cases, how-to guides contain the following sections: - Frontmatter with a guide title, description, and other information. The title should be short and identify the subject of the how-to topic in the fewest words possible. - The description should be a sentence that starts with a verb and summarizes the content of the topic. + The description should be a sentence that starts with a verb, summarizes the content of the topic, and ends with a period. Additional information might include a video banner link, a list of keywords, or an alternate first-level heading. @@ -138,7 +138,7 @@ Conceptual guides typically contain the following sections: - Frontmatter with a guide title, description, and other information. The title should be short and identify the subject of the how-to topic in the fewest words possible. - The description should be a sentence that starts with a verb and summarizes the content of the topic. + The description should be a sentence that starts with a verb, summarizes the content of the topic, and ends with a period. Additional information might include a list of keywords, or an alternate first-level heading. - Body paragraphs that explain that explain concepts, components, system operations, and context to help the reader understand what something is, why it's important, and how it works. @@ -164,7 +164,7 @@ Reference manuals typically contain the following sections: - Frontmatter with a guide title, description, and other information. The title should be short and identify the subject of the how-to topic in the fewest words possible. - The description should be a sentence that starts with a verb and summarizes the content of the topic. + The description should be a sentence that starts with a verb, summarizes the content of the topic, and ends with a period. Additional information might include a list of keywords. - One or more introductory paragraphs that explain what information the covers. - Formatted reference information. The format might resemble a `man` page or API description with a From 32adb818856f61baa526acd0737d3b15ce3e9a50 Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Fri, 19 Jul 2024 12:09:18 -0400 Subject: [PATCH 20/22] Allow four levels of sidebar entries (#486) Currently, the code that generates the navigation sidebar from a directory tree stops at the second level of a given top-level section. However, some sections include three levels of content. This change edits the sidebar generator so it works recursively. Also fix an issue with the `DocsNavigationItems` component that prevents the docs site from highlighting sidebar entries past two levels of depth. The component treats a sidebar subsection as "active" if one of its entries is equivalent to the current page path. But if the current page path is a grandchild of a sidebar subsection, this means that the component hides the grandchild, since none of the children of the subsection is equivalent to the current page. This change determines that a sidebar subsection is "active" if the selected path _starts with_ the subsection path. Also edit the CSS padding of navigation links to depend on the current level of the navigation menu. This allows for indentation of submenu links beyond the second level. --- .storybook/main.ts | 5 +- layouts/DocsPage/Navigation.module.css | 15 ++- layouts/DocsPage/Navigation.stories.tsx | 49 ++++++++++ layouts/DocsPage/Navigation.tsx | 34 ++++++- server/pages-helpers.ts | 69 ++++++-------- server/remark-toc.ts | 2 +- uvu-tests/config-docs.test.ts | 119 +++++++++++++++++------- 7 files changed, 205 insertions(+), 88 deletions(-) create mode 100644 layouts/DocsPage/Navigation.stories.tsx diff --git a/.storybook/main.ts b/.storybook/main.ts index 290946945a..472f128627 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,10 @@ import type { StorybookConfig } from "@storybook/nextjs"; const config: StorybookConfig = { - stories: ["../components/**/*.stories.@(js|jsx|ts|tsx)"], + stories: [ + "../components/**/*.stories.@(js|jsx|ts|tsx)", + "../layouts/**/*.stories.@(js|jsx|ts|tsx)", + ], addons: ["@storybook/addon-interactions", "@storybook/addon-viewport"], framework: { name: "@storybook/nextjs", diff --git a/layouts/DocsPage/Navigation.module.css b/layouts/DocsPage/Navigation.module.css index b3318fe14b..3d79d93ae6 100644 --- a/layouts/DocsPage/Navigation.module.css +++ b/layouts/DocsPage/Navigation.module.css @@ -107,12 +107,23 @@ display: block; } - & .link { - padding-left: var(--m-4); + & .link{ font-size: var(--fs-text-sm); line-height: var(--lh-md); } + & .link-1 { + padding-left: var(--m-4); + } + + & .link-2 { + padding-left: var(--m-5); + } + + & .link-3 { + padding-left: var(--m-6); + } + .link.active + & { display: block; } diff --git a/layouts/DocsPage/Navigation.stories.tsx b/layouts/DocsPage/Navigation.stories.tsx new file mode 100644 index 0000000000..cd3e40fc91 --- /dev/null +++ b/layouts/DocsPage/Navigation.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/testing-library"; +import { expect } from "@storybook/jest"; +import { default as DocNavigation } from "layouts/DocsPage/Navigation"; +import { NavigationCategory } from "./types"; + +export const NavigationFourLevels = () => { + const data = [ + { + icon: "cloud", + title: "Enroll Resources", + entries: [ + { + title: "Machine ID", + slug: "/enroll-resources/machine-id/", + entries: [ + { + title: "Deploy Machine ID", + slug: "/enroll-resources/machine-id/deployment/", + entries: [ + { + title: "Deploy Machine ID on AWS", + slug: "/enroll-resources/machine-id/deployment/aws/", + }, + ], + }, + ], + }, + ], + }, + ]; + + return ( + } + section={true} + currentVersion="16.x" + currentPathGetter={() => { + return "/enroll-resources/machine-id/deployment/aws/"; + }} + > + ); +}; + +const meta: Meta = { + title: "layouts/DocNavigation", + component: NavigationFourLevels, +}; +export default meta; diff --git a/layouts/DocsPage/Navigation.tsx b/layouts/DocsPage/Navigation.tsx index 07ad63b558..4de84d9678 100644 --- a/layouts/DocsPage/Navigation.tsx +++ b/layouts/DocsPage/Navigation.tsx @@ -24,14 +24,22 @@ const SCOPE_DICTIONARY: Record = { interface DocsNavigationItemsProps { entries: NavigationItem[]; onClick: () => void; + currentPath: string; + level?: number; } const DocsNavigationItems = ({ entries, onClick, + currentPath, + level, }: DocsNavigationItemsProps) => { - const docPath = useCurrentHref().split(SCOPELESS_HREF_REGEX)[0]; + const docPath = currentPath.split(SCOPELESS_HREF_REGEX)[0]; const { getVersionAgnosticRoute } = useVersionAgnosticPages(); + const maxLevel = 3; + if (!level) { + level = 1; + } return ( <> @@ -39,13 +47,15 @@ const DocsNavigationItems = ({ entries.map((entry) => { const selected = entry.slug === docPath; const active = - selected || entry.entries?.some((entry) => entry.slug === docPath); + selected || + entry.entries?.some((entry) => docPath.startsWith(entry.slug)); return (
  • )} - {!!entry.entries?.length && ( + {!!entry.entries?.length && level <= maxLevel && (
    )} @@ -77,6 +89,7 @@ interface DocNavigationCategoryProps extends NavigationCategory { opened: boolean; onToggleOpened: (value: number) => void; onClick: () => void; + currentPath: string; } const DocNavigationCategory = ({ @@ -87,6 +100,7 @@ const DocNavigationCategory = ({ icon, title, entries, + currentPath, }: DocNavigationCategoryProps) => { const toggleOpened = useCallback( () => onToggleOpened(opened ? null : id), @@ -105,7 +119,11 @@ const DocNavigationCategory = ({ {opened && (
      - +
    )} @@ -134,14 +152,19 @@ interface DocNavigationProps { section?: boolean; currentVersion?: string; data: NavigationCategory[]; + currentPathGetter?: () => string; } const DocNavigation = ({ data, section, currentVersion, + currentPathGetter, }: DocNavigationProps) => { - const route = useCurrentHref(); + if (!currentPathGetter) { + currentPathGetter = useCurrentHref; + } + const route = currentPathGetter(); const [openedId, setOpenedId] = useState( getCurrentCategoryIndex(data, route) @@ -171,6 +194,7 @@ const DocNavigation = ({ opened={index === openedId} onToggleOpened={setOpenedId} onClick={toggleMenu} + currentPath={route} {...props} />
  • diff --git a/server/pages-helpers.ts b/server/pages-helpers.ts index 4dfcb43e20..88f68f6f6e 100644 --- a/server/pages-helpers.ts +++ b/server/pages-helpers.ts @@ -62,6 +62,8 @@ export const getPageInfo = ( return result; }; +// getEntryForPath returns a navigation item for the file at filePath in the +// given filesystem. const getEntryForPath = (fs, filePath) => { const txt = fs.readFileSync(filePath, "utf8"); const { data } = matter(txt); @@ -108,11 +110,13 @@ const categoryPagePathForDir = (fs, dirPath) => { ); }; -export const generateNavPaths = (fs, dirPath) => { +export const navEntriesForDir = (fs, dirPath) => { const firstLvl = fs.readdirSync(dirPath, "utf8"); let result = []; let firstLvlFiles = new Set(); let firstLvlDirs = new Set(); + + // Sort the contents of dirPath into files and directoreis. firstLvl.forEach((p) => { const fullPath = join(dirPath, p); const info = fs.statSync(fullPath); @@ -120,11 +124,22 @@ export const generateNavPaths = (fs, dirPath) => { firstLvlDirs.add(fullPath); return; } + const fileName = parse(fullPath).name; + const dirName = parse(dirPath).name; + + // This is a category page for the containing directory. We would have + // already handled this in the previous iteration. The first iteration + // does not require a category page. + if (fileName == dirName) { + return; + } + firstLvlFiles.add(fullPath); }); // Map category pages to the directories they introduce so we can can add a - // sidebar entry for the category page, then traverse the directory. + // sidebar entry for each category page, then traverse each directory to add + // further sidebar pages. let sectionIntros = new Map(); firstLvlDirs.forEach((d: string) => { sectionIntros.set(categoryPagePathForDir(fs, d), d); @@ -145,6 +160,9 @@ export const generateNavPaths = (fs, dirPath) => { result.push(getEntryForPath(fs, f)); }); + // Add a category page for each section intro, then traverse the contents of + // the directory that the category page introduces, adding the contents to + // entries. sectionIntros.forEach((dirPath, categoryPagePath) => { const { slug, title } = getEntryForPath(fs, categoryPagePath); const section = { @@ -152,49 +170,14 @@ export const generateNavPaths = (fs, dirPath) => { slug: slug, entries: [], }; - const secondLvl = new Set(fs.readdirSync(dirPath, "utf8")); - - // Find all second-level category pages first so we don't - // repeat them in the sidebar. - secondLvl.forEach((f2: string) => { - let fullPath2 = join(dirPath, f2); - const stat = fs.statSync(fullPath2); - - // List category pages on the second level, but not their contents. - if (!stat.isDirectory()) { - return; - } - const catPath = categoryPagePathForDir(fs, fullPath2); - fullPath2 = catPath; - secondLvl.delete(f2); - - // Delete the category page from the set so we don't add it again - // when we add individual files. - secondLvl.delete(parse(catPath).base); - section.entries.push(getEntryForPath(fs, fullPath2)); - }); - - secondLvl.forEach((f2: string) => { - // Only add entries for MDX files here - if (!f2.endsWith(".mdx")) { - return; - } - - let fullPath2 = join(dirPath, f2); - - // This is a first-level category page that happens to exist on the second - // level. - if (sectionIntros.has(fullPath2)) { - return; - } - - const stat = fs.statSync(fullPath2); - section.entries.push(getEntryForPath(fs, fullPath2)); - }); - - section.entries.sort(sortByTitle); + + section.entries = navEntriesForDir(fs, dirPath); result.push(section); }); result.sort(sortByTitle); return result; }; + +export const generateNavPaths = (fs, dirPath) => { + return navEntriesForDir(fs, dirPath); +}; diff --git a/server/remark-toc.ts b/server/remark-toc.ts index 15bd2c88bd..d66e13936d 100644 --- a/server/remark-toc.ts +++ b/server/remark-toc.ts @@ -25,7 +25,7 @@ const relativePathToFile = (root: string, filepath: string) => { // properties: // - result: a string containing the resulting list of links. // - error: an error message encountered during processing -export const getTOC = (filePath: string, fs = nodeFS) => { +export const getTOC = (filePath: string, fs: any = nodeFS) => { const dirPath = path.dirname(filePath); if (!fs.existsSync(dirPath)) { return { diff --git a/uvu-tests/config-docs.test.ts b/uvu-tests/config-docs.test.ts index bdef7dc9a4..b7272875d6 100644 --- a/uvu-tests/config-docs.test.ts +++ b/uvu-tests/config-docs.test.ts @@ -289,54 +289,57 @@ title: MySQL Guide } ); -Suite( - "generateNavPaths shows third-level category pages on the sidebar", - () => { - const files = { - "/docs/pages/database-access/guides/guides.mdx": `--- +Suite("generateNavPaths shows third-level pages on the sidebar", () => { + const files = { + "/docs/pages/database-access/guides/guides.mdx": `--- title: Database Access Guides ---`, - "/docs/pages/database-access/guides/postgres.mdx": `--- + "/docs/pages/database-access/guides/postgres.mdx": `--- title: Postgres Guide ---`, - "/docs/pages/database-access/guides/mysql.mdx": `--- + "/docs/pages/database-access/guides/mysql.mdx": `--- title: MySQL Guide ---`, - "/docs/pages/database-access/guides/rbac/rbac.mdx": `--- + "/docs/pages/database-access/guides/rbac/rbac.mdx": `--- title: Database Access RBAC ---`, - "/docs/pages/database-access/guides/rbac/get-started.mdx": `--- + "/docs/pages/database-access/guides/rbac/get-started.mdx": `--- title: Get Started with DB RBAC ---`, - }; + }; - const expected = [ - { - title: "Database Access Guides", - slug: "/database-access/guides/guides/", - entries: [ - { - title: "Database Access RBAC", - slug: "/database-access/guides/rbac/rbac/", - }, - { - title: "MySQL Guide", - slug: "/database-access/guides/mysql/", - }, - { - title: "Postgres Guide", - slug: "/database-access/guides/postgres/", - }, - ], - }, - ]; + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "Database Access RBAC", + slug: "/database-access/guides/rbac/rbac/", + entries: [ + { + title: "Get Started with DB RBAC", + slug: "/database-access/guides/rbac/get-started/", + }, + ], + }, + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + ]; - const vol = Volume.fromJSON(files); - const fs = createFsFromVolume(vol); - const actual = generateNavPaths(fs, "/docs/pages/database-access"); - assert.equal(actual, expected); - } -); + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); +}); Suite( "allows category pages in the same directory as the associated subdirectory", @@ -367,6 +370,12 @@ title: Get Started with DB RBAC { title: "Database Access RBAC", slug: "/database-access/guides/rbac/", + entries: [ + { + title: "Get Started with DB RBAC", + slug: "/database-access/guides/rbac/get-started/", + }, + ], }, { title: "MySQL Guide", @@ -387,4 +396,42 @@ title: Get Started with DB RBAC } ); +Suite("generates four levels of the sidebar", () => { + const files = { + "/docs/pages/database-access/guides/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/deployment/kubernetes.mdx": `--- +title: Database Access Kubernetes Deployment +---`, + "/docs/pages/database-access/guides/deployment/deployment.mdx": `--- +title: Database Access Deployment Guides +---`, + }; + + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "Database Access Deployment Guides", + slug: "/database-access/guides/deployment/deployment/", + entries: [ + { + title: "Database Access Kubernetes Deployment", + slug: "/database-access/guides/deployment/kubernetes/", + }, + ], + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + let actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); +}); + Suite.run(); From b0531383a44411c9d0db5d4c457c739338277f62 Mon Sep 17 00:00:00 2001 From: "marie.mcallister" Date: Tue, 23 Jul 2024 17:10:55 -0700 Subject: [PATCH 21/22] new favicon for docs --- public/favicon.ico | Bin 1331 -> 4158 bytes public/favicon.svg | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/public/favicon.ico b/public/favicon.ico index 0b2174ae08b1324e32ff6f922e388c1a6b9f6fcf..c192f337b45a8e38828081e3854ccd322a091e08 100644 GIT binary patch literal 4158 zcmc&%?Qc_M6u(+H!U)CPa@7&(Iw$zq8F?*KVXZLx|`JLxE z?^jW7kOA_BWj& z%hFY|GPC4k-T(Dz6KEz#(`j;l&`6fW&lQ%<>+Ws)iJ0zIkLiH`b*Kb5AZ(d0>|4a zF*+vvzyV%>ar^f+5#(eK>|fn+h%8H3pG(WBXeJV+bSfps$2=36&Z9fZd%oi+b zN6cy~d#lmA;=$`W zBu#mIVy`Pty-L<)n%`l@yNB&K(J$d-D&WIFWDI9#oYTdHgceIjet``IpQ+uh)y+aHj6{b(FX^D)Pbacx#syKhP)c z#B#g`zNO^YgwUiDNeX^{UgAXV&oo0P&Kc*&p6HF&k-K2e?15Nz*EJev51u@4YzJG; zY3t03IK*B$kudi0PkJUruFapw-E0r7rE%3R`=E2avzsjUtYqJ)&ar7W0DJ-aphH}w z;~M_B+C{d9*9xwyb`Q&$^+}x{cNhIEUMaa{(%COt$#6uA!MmT8JduCq9DcTwh3Q~- z@L^24`UTIzxUeJShz&Gt=uh?YN?4rFQ(+xGBCGcUSrsEXlUIzUm z@Z&7U?3nfrQP&!--+Xu7cGKSQL9hvO+yC)xfU}`@FLrN# k(+?%DFjs>4{1%J__#2pGx8OH0_eS_eHZ%0^@cEkm0oJiuj{pDw literal 1331 zcmV-31Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$@ok>JNR9FeUS50VKRTRGG&Q!!M zLJ_g))*2PjlFUTAXl8=ef|8knofv|^e!m-+@ovC^WmbpqZwvbkS#5=8E0VYm;;s_Vem}RO zpG{PH8NtJFq7zJ*K1kkG0{^j-407U^MZx&-T_vED-r+Y6rlVjyOEP3W&$?D9zl(ib zqF8LEcwxuSMwb~VKY=~ZWABHItkXzD6(Y_TYQtv@E;D%UVQw^XDBhk#HhnOkCr5NP zfo!5O<~YuzF;GOm5c3QmC^&w}%9~c#71Iw0vVf70T(2U26kTzr9|z~3f?otAhvXkJ zI<571p_V$@bcO#iLl~bypj&OpMXlAbp1U`l{QUH`W#drB7j|9Txb)ApXBh4SFuc?KJ=}XMIbWC($nmyLZ>7!nNeBt!=3yQsr}ss7k|a- z@F~>Kflj`mJB$@5eTCC)Zx9d}o-fwZ+eNo`sB+IrBW8;>2ybNZSc9kMQ-2FT(qBH0 z?S0JX{39ke9mm-{iVpZ4+aSf$_CvwhuDHI;e=^^=s|^sstPuSRm5YtV)R9I*eX|;h z$sI!{jG?xOG0V5ncF%(rL-&X5mhR<^XAHmIQGAFmWADa5EQu+xZQIS&(^81X4G`OM z;geeOhT$=uf}=U}hZE!{s{?K1GD3dR=$byLYIuytTd`bGdz-M@Kux~wAX!$A6 zaD9~c5MRbxVmJ<+keCuXjx*p39PXnA;2)9HICxvpeqB@=XTZc-EX##5hS+_w1$}D) zO$jo5Jd-HB8l;bHptGd)%kn355T?~Bfu~>#-$wwA;;#NO$bl=njhB%eema>=O{3s0 zg(iS2)lrS2JjZ3vLUApHT*sM#$^qr5V&|JsU2g_BD{*>l+-z9otGI}ABplKXulll zzXwbI#+pZ|0d^X*1EqKHS@vcp3_j1M3iaVboiKi#Jqi87FdFaZ`7oW4N5L4Zj9n#g z2WkJc>i1)5&GDLLA`rF)B4Awz9 \ No newline at end of file + \ No newline at end of file From 920ab521222cd46946fd730d4dbb19257094409c Mon Sep 17 00:00:00 2001 From: Travis Swientek Date: Fri, 23 Aug 2024 16:40:53 -0500 Subject: [PATCH 22/22] Addition of PostHog 404 handler for additional insights. --- pages/404.tsx | 72 ++++++++++++++++++++++++++++++++++++------------ utils/posthog.ts | 6 +++- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/pages/404.tsx b/pages/404.tsx index 06d71824c8..d5c7de32b5 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -1,27 +1,63 @@ // 404.js import Link from "next/link"; +import { sendPageNotFoundError } from "utils/posthog"; +import { useEffect } from "react"; +import SiteHeader from "components/Header"; +import Footer from "layouts/DocsPage/Footer"; export default function FourOhFour() { + useEffect(() => { + void sendPageNotFoundError(); // Report Error to PostHog for tracking + }, []); + return ( -
    -

    404 Page Not Found

    -

    Sorry, we couldn't find that page

    -

    We couldn't find the page you were looking for.

    -

    Pages you may find useful

    -
      -
    • Home Page
    • -
    • About Us
    • -
    • Blog
    • -
    • Customer Support
    • -
    • Documentation
    • -
    • Installation
    • -
    • Teleport Server Access
    • -
    • Teleport Kubernetes Access
    • -
    • Teleport Database Access
    • -
    • Teleport Desktop Access
    • -
    • Teleport Application Access
    • -
    +
    + +
    +

    404 Page Not Found

    +

    Sorry, we couldn't find that page

    +

    + Go back to Documentation home? +

    +

    Other pages you may find useful

    +
      +
    • + Home Page +
    • +
    • + About Us +
    • +
    • + Blog +
    • +
    • + Customer Support +
    • +
    • + Documentation +
    • +
    • + Installation +
    • +
    • + Teleport Server Access +
    • +
    • + Teleport Kubernetes Access +
    • +
    • + Teleport Database Access +
    • +
    • + Teleport Desktop Access +
    • +
    • + Teleport Application Access +
    • +
    +
    +
    ); } diff --git a/utils/posthog.ts b/utils/posthog.ts index f1108289d2..80e096f7d2 100644 --- a/utils/posthog.ts +++ b/utils/posthog.ts @@ -16,7 +16,6 @@ export const posthog = async (): Promise => { return; } - if (PH_IS_ENABLED && PH_API_URL && PH_API_KEY) { posthogGlobal.init(PH_API_KEY, { api_host: PH_API_URL, @@ -41,6 +40,11 @@ export const sendPageview = async () => { ph?.capture("$pageview"); }; +export const sendPageNotFoundError = async () => { + const ph = await posthog(); + ph?.capture("web.errors.pageNotFound"); +}; + export const sendDocsFeedback = async (rating: string, comment: string) => { const ph = await posthog();