From 315c61184d070a11c685450e6697c6e1758c76bc Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Tue, 22 Aug 2023 12:45:31 -0400 Subject: [PATCH 1/2] Remove scoped visibility from the docs engine Closes #343 Closes #162 Closes #155 Closes #118 Closes #116 Closes #88 Users tend not to notice the scope switcher. When they do, the component's behavior is not intuitive. This change removes the scope switcher from the docs, replacing it with a non-interactive list of supported Teleport editions. The change preserves the existing styling of the scope switcher as much as possible. Aside from an "Available for" label next to the edition list, the change should go unnoticed for users who do not interact with the scope switcher. This change also adjust some docs behavior that relied on the scope switcher: - **The `DocsContext` no longer includes the current scope:** Components no longer refer to the `DocsContext` to determine the currently selected scope and adjust their visibility. "Box" components like the `Details`, `Notice`, and `Admonition` component can currently hide themselves if the selected scope does not match the value of the `scope` prop. This change shows these boxes at all times, but keeps the `scope` and `scopeOnly` props for backwards compatability (the props are no-op). The `Tabs` component currently uses the selected scope to select a `TabItem` with the `scope` property. Since we merged #380, though, `Tabs` components can preserve most of this behavior without the scope switcher, as long as these components include identical labels for edition-related tab items. This change also retains the `scope` prop in no-op form. - **Locking the version picker if the scope is `cloud` or `team`**: This change no longer locks the version picker, since there is no way to select a scope. Locking the version picker becomes an issue when Teleport Cloud is a major version behind the latest release. Removing the scope switcher doesn't completely rectify this, since the version of the docs for the previous version includes a warning banner. We can address this in a future change. --- components/Admonition/Admonition.tsx | 16 +------ components/Details/Details.tsx | 36 ++-------------- components/Link/hooks.ts | 17 +------- components/Notice/Notice.tsx | 15 +------ components/ScopedBlock/ScopedBlock.tsx | 15 ------- components/ScopedBlock/index.ts | 1 - components/Tabs/Tabs.tsx | 15 +------ layouts/DocsPage/DocsPage.tsx | 12 +----- layouts/DocsPage/Header.tsx | 2 - layouts/DocsPage/Navigation.tsx | 2 - layouts/DocsPage/Scopes.module.css | 48 +++++++++++++++++++++ layouts/DocsPage/Scopes.tsx | 39 +++++++---------- layouts/DocsPage/components.tsx | 2 - layouts/DocsPage/context.tsx | 60 +------------------------- 14 files changed, 79 insertions(+), 201 deletions(-) delete mode 100644 components/ScopedBlock/ScopedBlock.tsx delete mode 100644 components/ScopedBlock/index.ts diff --git a/components/Admonition/Admonition.tsx b/components/Admonition/Admonition.tsx index 7186bbdcdd..c8dbe236a5 100644 --- a/components/Admonition/Admonition.tsx +++ b/components/Admonition/Admonition.tsx @@ -1,6 +1,6 @@ import cn from "classnames"; import { useContext, useMemo } from "react"; -import { DocsContext, getScopes } from "layouts/DocsPage/context"; +import { DocsContext } from "layouts/DocsPage/context"; import styles from "./Admonition.module.css"; const capitalize = (text: string): string => @@ -12,7 +12,6 @@ export interface AdmonitionProps { type: (typeof types)[number]; title: string; children: React.ReactNode; - scopeOnly: boolean; scope?: string | string[]; } @@ -20,23 +19,12 @@ const Admonition = ({ type = "tip", title, children, - scopeOnly = false, scope, }: AdmonitionProps) => { const resolvedType = type && types.includes(type) ? type : "tip"; - const { scope: currentScope } = useContext(DocsContext); - const scopes = useMemo(() => getScopes(scope), [scope]); - const isInCurrentScope = scopes.includes(currentScope); - const isHidden = scopeOnly && !isInCurrentScope; return ( -
+
{title || capitalize(type)}
{children}
diff --git a/components/Details/Details.tsx b/components/Details/Details.tsx index 25f206cfb8..a0fa84a8ef 100644 --- a/components/Details/Details.tsx +++ b/components/Details/Details.tsx @@ -3,7 +3,7 @@ import { useContext, useEffect, useState, useMemo } from "react"; import { useRouter } from "next/router"; import HeadlessButton from "components/HeadlessButton"; import Icon from "components/Icon"; -import { DocsContext, getScopes } from "layouts/DocsPage/context"; +import { DocsContext } from "layouts/DocsPage/context"; import { getAnchor } from "utils/url"; import styles from "./Details.module.css"; @@ -19,54 +19,35 @@ export interface DetailsProps { scope?: string | string[]; title: string; opened?: boolean; - scopeOnly: boolean; min: string; children: React.ReactNode; } export const Details = ({ scope, - scopeOnly = false, opened = false, title, min, children, }: DetailsProps) => { const { - scope: currentScope, versions: { current, latest }, } = useContext(DocsContext); const router = useRouter(); - const scopes = useMemo(() => getScopes(scope), [scope]); const [isOpened, setIsOpened] = useState(opened); - const isInCurrentScope = scopes.includes(currentScope); const detailsId = title ? transformTitleToAnchor(title) : "title"; const anchorInPath = getAnchor(router.asPath); - useEffect(() => { - if (scopes.length) { - setIsOpened(isInCurrentScope); - } - }, [scopes, isInCurrentScope]); - useEffect(() => { if (anchorInPath === detailsId) { setIsOpened(true); } }, [anchorInPath, detailsId]); - const isCloudAndNotCurrent = scopes.includes("cloud") && current !== latest; - const isHiddenInCurrentScope = scopeOnly && !isInCurrentScope; - const isHidden = isCloudAndNotCurrent || isHiddenInCurrentScope; - return (
setIsOpened((value) => !value)} @@ -75,17 +56,8 @@ export const Details = ({
{title}
- {(scope || min) && ( + {min && (
- {scopes && ( -
- {scopes.map((scope) => ( -
- {scope} -
- ))} -
- )} {min &&
VERSION {min}+
}
)} diff --git a/components/Link/hooks.ts b/components/Link/hooks.ts index f881eb7992..44d6d5e6a9 100644 --- a/components/Link/hooks.ts +++ b/components/Link/hooks.ts @@ -9,7 +9,7 @@ import { isExternalLink, isLocalAssetFile, } from "utils/url"; -import { DocsContext, updateScopeInUrl } from "layouts/DocsPage/context"; +import { DocsContext } from "layouts/DocsPage/context"; /* * This hook should return current href with resolved rewrites @@ -35,8 +35,6 @@ export const useNormalizedHref = (href: string) => { ? href.substring(basePath.length) : href; - let scope: ScopeType = useContext(DocsContext).scope; - const { query } = splitPath(href); // This needs to be added because all strings of "/docs/" are being stripped down to @@ -46,15 +44,6 @@ export const useNormalizedHref = (href: string) => { return href; } - // If a valid scope is provided via query parameter, adjust the - // link to navigate to that scope. - if ( - query.hasOwnProperty("scope") && - scopeValues.includes(query.scope as ScopeType) - ) { - scope = query["scope"] as ScopeType; - } - if ( isHash(noBaseHref) || isExternalLink(noBaseHref) || @@ -65,7 +54,5 @@ export const useNormalizedHref = (href: string) => { const currentHref = normalizePath(asPath); - let fullHref = resolve(splitPath(currentHref).path, noBaseHref); - - return updateScopeInUrl(fullHref, scope); + return resolve(splitPath(currentHref).path, noBaseHref); }; diff --git a/components/Notice/Notice.tsx b/components/Notice/Notice.tsx index 04e21695c1..cfc24512d7 100644 --- a/components/Notice/Notice.tsx +++ b/components/Notice/Notice.tsx @@ -1,7 +1,5 @@ import { useMemo, useContext } from "react"; import Icon from "components/Icon"; -import { DocsContext, getScopes } from "layouts/DocsPage/context"; -import { ScopesType } from "layouts/DocsPage/types"; import cn from "classnames"; import styles from "./Notice.module.css"; @@ -21,7 +19,7 @@ export interface NoticeProps { children: React.ReactNode; className?: string; icon?: boolean; - scope?: ScopesType; + scope?: string; } const Notice = ({ @@ -34,17 +32,8 @@ const Notice = ({ }: NoticeProps) => { const type = baseType && types.includes(baseType) ? baseType : "tip"; const iconName = typeIcons[type]; - const { scope: currentScope } = useContext(DocsContext); - const scopes = useMemo(() => getScopes(scope), [scope]); - - const isHidden = scope && !scopes.includes(currentScope); - return ( -
+
{icon && }
{children}
diff --git a/components/ScopedBlock/ScopedBlock.tsx b/components/ScopedBlock/ScopedBlock.tsx deleted file mode 100644 index 4f91a82df9..0000000000 --- a/components/ScopedBlock/ScopedBlock.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useMemo, useContext } from "react"; -import { DocsContext, getScopes } from "layouts/DocsPage/context"; - -interface ScopedBlockProps { - scope: string | string[]; - children: React.ReactNode; -} - -export const ScopedBlock = ({ scope, children }: ScopedBlockProps) => { - const { scope: currentScope } = useContext(DocsContext); - const scopes = useMemo(() => getScopes(scope), [scope]); - const isInCurrentScope = scopes.includes(currentScope); - - return isInCurrentScope ? <>{children} : null; -}; diff --git a/components/ScopedBlock/index.ts b/components/ScopedBlock/index.ts deleted file mode 100644 index b729fbf34b..0000000000 --- a/components/ScopedBlock/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ScopedBlock as default } from "./ScopedBlock"; diff --git a/components/Tabs/Tabs.tsx b/components/Tabs/Tabs.tsx index a7c70fbd92..3d3c4be4a3 100644 --- a/components/Tabs/Tabs.tsx +++ b/components/Tabs/Tabs.tsx @@ -6,7 +6,7 @@ import { useMemo, } from "react"; import { Dropdown } from "components/Dropdown"; -import { DocsContext, getScopes } from "layouts/DocsPage/context"; +import { DocsContext } from "layouts/DocsPage/context"; import { TabLabelList } from "./TabLabel"; import { TabItemList } from "./TabItem"; import { DataTab, TabsInDropdowns, TabItemProps, TabsProps } from "./types"; @@ -46,7 +46,7 @@ import { TabContext } from "./TabContext"; ``` - + ```code # Checkout teleport-plugins $ git clone https://github.com/gravitational/teleport-plugins.git @@ -78,7 +78,6 @@ export const Tabs = ({ dropdownView, }: TabsProps) => { const { - scope, versions: { latest, current }, } = useContext(DocsContext); @@ -168,16 +167,6 @@ export const Tabs = ({ setCurrentTab(getSelectedTab(tabsMeta)); }, [tabsMeta, selectedDropdownOption]); - useEffect(() => { - const scopedTab = childTabs.find(({ props }) => - getScopes(props.scope).includes(scope) - ); - - if (scopedTab) { - setCurrentTab(scopedTab.props.label); - } - }, [scope, childTabs]); - const visibleTabs = dropdownVarsArr.filter((t) => t !== DEFAULT_DROPDOWN); const dropOptions = tabsMeta.map((item) => item.label); diff --git a/layouts/DocsPage/DocsPage.tsx b/layouts/DocsPage/DocsPage.tsx index 97309c815f..e4039cbd72 100644 --- a/layouts/DocsPage/DocsPage.tsx +++ b/layouts/DocsPage/DocsPage.tsx @@ -45,22 +45,14 @@ const DocsPage = ({ }: DocsPageProps) => { const route = useCurrentHref(); - const { setVersions, scope, setScope } = useContext(DocsContext); + const { setVersions } = useContext(DocsContext); const { current, latest, available } = versions; const getPath = useFindDestinationPath(versions); useEffect(() => { setVersions(versions); - - if ( - scopes[0] !== "" && - scopes[0] !== "noScope" && - !scopes.includes(scope) - ) { - setScope(scopes[0]); - } - }, [versions, setVersions, setScope, scopes, scope]); + }, [versions, setVersions]); const isSectionLayout = layout === "section"; const isTocVisible = (!layout || layout === "doc") && tableOfContents.length; diff --git a/layouts/DocsPage/Header.tsx b/layouts/DocsPage/Header.tsx index 46e5725f6b..6fcc39a1ac 100644 --- a/layouts/DocsPage/Header.tsx +++ b/layouts/DocsPage/Header.tsx @@ -30,7 +30,6 @@ const DocHeader = ({ latest, scopes, }: DocHeaderProps) => { - const { scope } = useContext(DocsContext); return (
@@ -53,7 +52,6 @@ const DocHeader = ({ {...versions} className={styles.versions} getNewVersionPath={getNewVersionPath} - disabled={scope === "cloud" || scope === "team"} latest={latest} /> )} diff --git a/layouts/DocsPage/Navigation.tsx b/layouts/DocsPage/Navigation.tsx index 4e750204fc..ac1ea084a3 100644 --- a/layouts/DocsPage/Navigation.tsx +++ b/layouts/DocsPage/Navigation.tsx @@ -5,7 +5,6 @@ import HeadlessButton from "components/HeadlessButton"; import Search from "components/Search"; import Icon from "components/Icon"; import Link, { useCurrentHref } from "components/Link"; -import { getScopeFromUrl } from "./context"; import { NavigationItem, NavigationCategory, @@ -63,7 +62,6 @@ const DocsNavigationItems = ({ }: DocsNavigationItemsProps) => { const router = useRouter(); const docPath = useCurrentHref().split(SCOPELESS_HREF_REGEX)[0]; - const urlScope = getScopeFromUrl(router.asPath); return ( <> diff --git a/layouts/DocsPage/Scopes.module.css b/layouts/DocsPage/Scopes.module.css index 78a5a49dbb..74632956f0 100644 --- a/layouts/DocsPage/Scopes.module.css +++ b/layouts/DocsPage/Scopes.module.css @@ -19,3 +19,51 @@ margin-left: var(--m-1); } } + +.label { + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + height: 32px; + padding: 0 var(--m-2); + font-size: var(--fs-text-sm); + font-weight: 600; + border-radius: var(--r-default); + transition: background-color var(--t-interaction), + border-color var(--t-interaction); + + &.variant-availablefor { + color: var(--color-black); + background-color: var(--color-white); + } + + &.variant-gray { + color: var(--color-gray); + background-color: var(--color-white); + } + + &.variant-green { + color: var(--color-green); + background-color: var(--color-white); + } + + &.variant-blue { + color: var(--color-light-blue); + background-color: var(--color-white); + } + + @media (--sm-scr) { + font-size: var(--fs-text-md); + } +} + +.icon { + width: 14px; + height: 14px; + margin-right: var(--m-0-5); +} + +.wrapper { + position: relative; +} diff --git a/layouts/DocsPage/Scopes.tsx b/layouts/DocsPage/Scopes.tsx index 6dce26a78b..83deaa3c8f 100644 --- a/layouts/DocsPage/Scopes.tsx +++ b/layouts/DocsPage/Scopes.tsx @@ -6,6 +6,7 @@ import type { ScopeType, ScopesInMeta } from "./types"; import type { IconName } from "components/Icon"; import type { RadioButtonVariant } from "components/RadioButton"; import styles from "./Scopes.module.css"; +import Icon from "components/Icon"; interface ScopeDescription { value: ScopeType; @@ -46,22 +47,19 @@ const SCOPE_DESCRIPTIONS: Record< interface ScopesItemProps { scopeFeatures: ScopeDescription; - currentScope: ScopeType; } -const ScopesItem = ({ scopeFeatures, currentScope }: ScopesItemProps) => { +const ScopesItem = ({ scopeFeatures }: ScopesItemProps) => { return (
  • - +
    + + {" "} + {scopeFeatures.title} + +
  • ); }; @@ -72,27 +70,22 @@ interface ScopesProps { } export const Scopes = ({ scopes, className }: ScopesProps) => { - const { scope, setScope } = useContext(DocsContext); - - const onChange = useCallback( - (event) => { - setScope(event.target.value); - }, - [setScope] - ); - if (scopes[0] === "noScope" || scopes[0] === "") return <>; const scopeItems = scopes?.map((item) => ( )); return ( -
      +
        +
      • + + Available for: + +
      • {scopeItems}
      ); diff --git a/layouts/DocsPage/components.tsx b/layouts/DocsPage/components.tsx index cd7ea91458..1e66feedb5 100644 --- a/layouts/DocsPage/components.tsx +++ b/layouts/DocsPage/components.tsx @@ -3,7 +3,6 @@ import Command, { CommandLine, CommandComment } from "components/Command"; import Icon from "components/Icon"; import InlineCode from "components/InlineCode"; import Notice from "components/Notice"; -import ScopedBlock from "components/ScopedBlock"; import Snippet from "components/Snippet"; import { Tabs, TabItem } from "components/Tabs"; import { @@ -70,7 +69,6 @@ export const components = { commandcomment: CommandComment, icon: Icon, inlinecode: InlineCode, - scopedblock: ScopedBlock, tabs: Tabs, tabitem: TabItem, tile: Tile, diff --git a/layouts/DocsPage/context.tsx b/layouts/DocsPage/context.tsx index a3ff6f745d..f4d84df741 100644 --- a/layouts/DocsPage/context.tsx +++ b/layouts/DocsPage/context.tsx @@ -3,48 +3,7 @@ import { createContext, useState, useEffect } from "react"; import { splitPath, buildPath } from "utils/url"; import { VersionsInfo, scopeValues, ScopeType } from "./types"; -export const getScopes = (scopes?: string | string[]): ScopeType[] => { - if (typeof scopes === "string") { - return scopes - .split(",") - .map((scope) => scope.trim()) - .filter((scope) => - scopeValues.includes(scope as ScopeType) - ) as ScopeType[]; - } else if (Array.isArray(scopes)) { - return scopes.filter((scope) => - scopeValues.includes(scope as ScopeType) - ) as ScopeType[]; - } else { - return []; - } -}; - -export const getScopeFromUrl = (asPath: string): ScopeType => { - const { query } = splitPath(asPath); - - const scope = query.scope as ScopeType; - - return scopeValues.includes(scope) ? scope : "oss"; -}; - -export const updateScopeInUrl = (asPath: string, scope: ScopeType): string => { - const urlParts = splitPath(asPath); - - if (scope === "oss") { - if (urlParts.query.scope) { - delete urlParts.query.scope; - } - } else { - urlParts.query.scope = scope; - } - - return buildPath(urlParts); -}; - export interface DocsContextProps { - scope: ScopeType; - setScope: (scope: ScopeType) => void; versions: VersionsInfo; setVersions: (version: VersionsInfo) => void; } @@ -56,8 +15,6 @@ const defaultVersions = { }; export const DocsContext = createContext({ - scope: "oss", - setScope: () => undefined, versions: defaultVersions, setVersions: () => undefined, }); @@ -72,9 +29,7 @@ export const DocsContextProvider = ({ children }: DocsContextProviderProps) => { ? router.asPath.split("/")[2] : ""; - const urlScope = getScopeFromUrl(router.asPath); const [ready, setReady] = useState(false); - const [scope, setScope] = useState("oss"); const [versions, setVersions] = useState({ ...defaultVersions, current, @@ -83,26 +38,13 @@ export const DocsContextProvider = ({ children }: DocsContextProviderProps) => { // We set these variables to prevent incosistency with ssr useEffect(() => { if (!ready) { - setScope(urlScope); setReady(true); - } else { - if (scope === "cloud" && versions.current !== versions.latest) { - router.replace("/?scope=cloud"); - } else if (scope !== urlScope) { - router.replace( - updateScopeInUrl(router.asPath, scope), - updateScopeInUrl(router.asPath, scope), - { shallow: true } - ); - } } - }, [scope, ready, urlScope, router, versions]); + }, [ready, router, versions]); return ( Date: Tue, 5 Sep 2023 15:07:18 -0400 Subject: [PATCH 2/2] Fix formatting --- layouts/DocsPage/Header.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/layouts/DocsPage/Header.tsx b/layouts/DocsPage/Header.tsx index 6fcc39a1ac..46318db9c9 100644 --- a/layouts/DocsPage/Header.tsx +++ b/layouts/DocsPage/Header.tsx @@ -30,7 +30,6 @@ const DocHeader = ({ latest, scopes, }: DocHeaderProps) => { - return (