From 48d4a5d9f3bbd207a97eecfc91477cc5921b9313 Mon Sep 17 00:00:00 2001 From: Oreoluwa Agunbiade <21035422+oredavids@users.noreply.github.com> Date: Fri, 17 Mar 2023 08:40:40 -0600 Subject: [PATCH] [CORE-624] Notify users when clicking enterprise subscription links (#677) * Notify users when clicking enterprise subscription links * Remove debug logs * Add netlify.toml file * Make ts-commons optional dependency * Optionally use ts-commons * Update ts-commons dynamic import * Remove netflify.toml file * Update ts-commons util documentation --- .gitignore | 3 + package.json | 3 +- src/components/.d.ts | 1 + src/components/SubscribersOnlyModal.tsx | 42 ++++---- src/theme/Root.js | 136 ++++++++++++++++-------- utils/index.ts | 1 + utils/ts-commons.ts | 31 ++++++ yarn.lock | 4 + 8 files changed, 157 insertions(+), 64 deletions(-) create mode 100644 src/components/.d.ts create mode 100644 utils/index.ts create mode 100644 utils/ts-commons.ts diff --git a/.gitignore b/.gitignore index 514c5aaa8..355606efe 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,6 @@ build/ # IntelliJ files .idea *.iml + +# VS Code files +.vscode diff --git a/package.json b/package.json index 0986a1083..dded26483 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "yargs": "^17.4.0" }, "optionalDependencies": { - "docs-sourcer": "git+ssh://git@github.com/gruntwork-io/docs-sourcer.git#v0.2" + "docs-sourcer": "git+ssh://git@github.com/gruntwork-io/docs-sourcer.git#v0.2", + "ts-commons": "gruntwork-io/ts-commons#v1.0.0" }, "browserslist": { "production": [ diff --git a/src/components/.d.ts b/src/components/.d.ts new file mode 100644 index 000000000..2ab174fdb --- /dev/null +++ b/src/components/.d.ts @@ -0,0 +1 @@ +declare module "*.module.css" diff --git a/src/components/SubscribersOnlyModal.tsx b/src/components/SubscribersOnlyModal.tsx index 93e5dbdaa..fa75e33cd 100644 --- a/src/components/SubscribersOnlyModal.tsx +++ b/src/components/SubscribersOnlyModal.tsx @@ -2,13 +2,17 @@ import React from "react" import { Modal } from "./Modal" import styles from "./Modal.module.css" +export const gruntworkGithubOrg = "https://github.com/gruntwork-io/" + +/** @type {RegExp} Match a link prefixed by the gruntworkGithubOrg and capture the next path reference */ +export const repoNamePattern = new RegExp(`^${gruntworkGithubOrg}(.*?)(\/|$)`) + interface SubscribersOnlyModalProps { externalLink: string localStorageKey: string subscriberType?: string showModal: boolean - handleCancelRequest: () => void - handleAcceptRequest?: () => void + clearLink: () => void } export const SubscribersOnlyModal: React.FC = ({ @@ -16,8 +20,7 @@ export const SubscribersOnlyModal: React.FC = ({ localStorageKey, subscriberType, showModal, - handleCancelRequest, - handleAcceptRequest, + clearLink, }) => { const onRequestClose = (e) => { // If the user checked to never see this notice but subsequently cancels we will disregard their selection. We will @@ -26,15 +29,18 @@ export const SubscribersOnlyModal: React.FC = ({ window.localStorage.removeItem(localStorageKey) } - handleCancelRequest() + clearLink() + e.preventDefault() // prevent the browser from handling a Cancel button click and scrolling to top + } - // prevent the browser from handling a Cancel button click and scrolling to top - e.preventDefault() + const repoNameMatchArray: RegExpMatchArray | null = + externalLink.match(repoNamePattern) + + if (!repoNameMatchArray) { + return <> // The link is not a Gruntwork Github repo link } - const gitHubRepoName = externalLink.match( - /https:\/\/github.com\/gruntwork-io\/([^/]*)/ - ) + const repoName = repoNameMatchArray[1] const setDontWarnMe = (event) => { event.stopPropagation() @@ -51,8 +57,8 @@ export const SubscribersOnlyModal: React.FC = ({ shouldCloseOnEsc={true} shouldAcceptOnEnter={false} shouldCloseOnOverlayClick={true} - handleCancelRequest={handleCancelRequest} - handleAcceptRequest={handleAcceptRequest} + handleCancelRequest={clearLink} + handleAcceptRequest={clearLink} >

{subscriberType @@ -60,18 +66,16 @@ export const SubscribersOnlyModal: React.FC = ({ : "For Subscribers Only"}

- This link leads to the private{" "} - {gitHubRepoName && gitHubRepoName.length >= 1 && ( - {gitHubRepoName[1]} - )}{" "} - repository visible only to subscribers; everyone else will see a 404. + This link leads to the private {repoName} repository + visible only to {subscriberType && `${subscriberType} `} + subscribers; everyone else will see a 404.

- onRequestClose(e)} href="#"> + Cancel = ({ target="_blank" data-modal-exempt={true} onClick={() => { - handleAcceptRequest() + setTimeout(clearLink, 500) // Wait .5seconds to allow propagation to external link before clearing the link from state }} > Continue to GitHub diff --git a/src/theme/Root.js b/src/theme/Root.js index 3aebc477a..4b7d0911d 100644 --- a/src/theme/Root.js +++ b/src/theme/Root.js @@ -1,9 +1,18 @@ -import React, { useState, useEffect } from "react" -import { SubscribersOnlyModal } from "/src/components/SubscribersOnlyModal.tsx" - -const gruntworkGithubOrg = "https://github.com/gruntwork-io/" +/** + * This file is the mechanism for adding stateful logic to a docusaurus + * application since the component is rendered at the very top of the + * React tree and never unmounts. + * We swizzle(customize) the Root component by creating this file: Root.js + * https://docusaurus.io/docs/swizzling#wrapper-your-site-with-root + */ -const gruntworkCisRepoName = "terraform-aws-cis-service-catalog" +import React, { useState, useEffect } from "react" +import { getRepos } from "/utils" +import { + SubscribersOnlyModal, + repoNamePattern, + gruntworkGithubOrg, +} from "/src/components/SubscribersOnlyModal.tsx" const publicGruntworkRepoNames = [ "bash-commons", @@ -53,29 +62,46 @@ const publicGruntworkRepoNames = [ "website-comments", ] +const { awsCISRepos, enterpriseRepos } = getRepos() + /** - * Checks if a link references a known public Gruntwork repo + * Checks if a given list of repo names includes a repo name extracted from a given url that matches the repoNamePattern * - * @param string url + * @param {string[]} repoNames + * @param {string} url * @return {boolean} */ -const isPublicGruntworkRepo = (url) => { +const listIncludesRepo = (repoNames, url) => { if (!url) { return false } - // Match a link prefixed by the gruntworkGithubOrg and capture the next path reference - const pattern = new RegExp(`^${gruntworkGithubOrg}(.*?)(\/|$)`) - // e.g for a given link https://github.com/gruntwork-io/docs/intro -> `docs` - const repoName = url.match(pattern)[1] + + const repoMatchArray = url.match(repoNamePattern) + if (!repoMatchArray) { + return false + } + + const repoName = repoMatchArray[1] // e.g for a given link https://github.com/gruntwork-io/docs/intro -> `docs` // returns boolean - return publicGruntworkRepoNames.includes(repoName) + return repoNames.includes(repoName) +} + +/** + * Checks if a link references a known public Gruntwork repo + * + * @param {string} url + * @return {boolean} + */ +const isPublicGruntworkRepo = (url) => { + // returns boolean + return listIncludesRepo(publicGruntworkRepoNames, url) } /** * Checks if a link references a private Gruntwork repo * - * @param string url + * @param {string} url * @return {boolean} */ const isPrivateGruntworkRepo = (url) => { @@ -85,33 +111,49 @@ const isPrivateGruntworkRepo = (url) => { } /** - * Checks if a link references the Gruntwork CIS service catalog repo + * Checks if a link references a Gruntwork CIS repo * - * @param string url + * @param {string} url * @return {boolean} */ - const isGruntworkCisRepo = (url) => { - return url && url.startsWith(`${gruntworkGithubOrg}${gruntworkCisRepoName}`) + // awsCISRepos is an array of strings, e.g. `gruntwork-io/cis-docs` + const cisRepoNames = awsCISRepos.map((repo) => repo.split("/")[1]) + return listIncludesRepo(cisRepoNames, url) +} + +/** + * Checks if a link references a Gruntwork Enterprise repo + * + * @param {string} url + * @return {boolean} + */ +const isGruntworkEnterpriseRepo = (url) => { + // enterpriseRepos is an array of strings, e.g. `gruntwork-io/enterprise-docs` + const enterpriseRepoNames = enterpriseRepos.map((repo) => repo.split("/")[1]) + return listIncludesRepo(enterpriseRepoNames, url) } export const DONT_SHOW_PRIVATE_GITHUB_WARNING_KEY = "dontWarnGitHubLinks" export const DONT_SHOW_CIS_GITHUB_WARNING_KEY = "dontWarnCISLinks" +export const DONT_SHOW_ENTERPRISE_GITHUB_WARNING_KEY = "dontWarnEnterpriseLinks" function Root({ children }) { - const [displaySubscriberNotice, setDisplaySubscriberNotice] = useState(false) const [subscriberNoticeLink, setSubscriberNoticeLink] = useState("") - - const [displayCisNotice, setDisplayCisNotice] = useState(false) const [cisNoticeLink, setCisNoticeLink] = useState("") + const [enterpriseNoticeLink, setEnterpriseNoticeLink] = useState("") - useEffect(() => { + useEffect(function showModalForPrivateGithubLinks() { const listener = (event) => { // Sometimes our links wrap components, such as Cards. In these cases, the event // target is often a child element of the we're attempting to extract the // href data from, and so we search for the closest parent . In the event that // an is clicked directly, that itself will be returned. - const targetLink = event.target.closest("a") + const targetLink = event?.target?.closest("a") + + if (!targetLink || !targetLink.href) { + return + } // Allow clicks on the external GitHub link FROM the modal notices to work normally if (targetLink.dataset.modalExempt) { @@ -124,13 +166,25 @@ function Root({ children }) { ) if (dontWarn) { - setDisplayCisNotice(false) return } - event.preventDefault() + event.preventDefault() // This prevents the link from opening & ensures the modal is displayed setCisNoticeLink(targetLink.href) - setDisplayCisNotice(true) + return + } + + if (isGruntworkEnterpriseRepo(targetLink.href)) { + const dontWarn = window.localStorage.getItem( + DONT_SHOW_ENTERPRISE_GITHUB_WARNING_KEY + ) + + if (dontWarn) { + return + } + + event.preventDefault() // This prevents the link from opening & ensures the modal is displayed + setEnterpriseNoticeLink(targetLink.href) return } @@ -144,9 +198,8 @@ function Root({ children }) { return } - event.preventDefault() + event.preventDefault() // This prevents the link from opening & ensures the modal is displayed setSubscriberNoticeLink(targetLink.href) - setDisplaySubscriberNotice(true) return } } @@ -160,29 +213,24 @@ function Root({ children }) { return ( <> { - setDisplaySubscriberNotice(false) - setSubscriberNoticeLink("") - }} - handleAcceptRequest={() => { - setDisplaySubscriberNotice(false) - }} + clearLink={() => setSubscriberNoticeLink("")} /> { - setDisplayCisNotice(false) - setCisNoticeLink("") - }} - handleAcceptRequest={() => { - setDisplayCisNotice(false) - }} + clearLink={() => setCisNoticeLink("")} + /> + setEnterpriseNoticeLink("")} /> {children} diff --git a/utils/index.ts b/utils/index.ts new file mode 100644 index 000000000..55e83f435 --- /dev/null +++ b/utils/index.ts @@ -0,0 +1 @@ +export * from "./ts-commons" diff --git a/utils/ts-commons.ts b/utils/ts-commons.ts new file mode 100644 index 000000000..5d35226c6 --- /dev/null +++ b/utils/ts-commons.ts @@ -0,0 +1,31 @@ +/** + * Get repos from ts-commons package if available. Otherwise return empty arrays. + * + * ts-commons package is a private package so some users may not have access to + * it. Use this function to safely pull in an optional package to avoid both + * build-time and runtime errors. + * + * @return {*} {{ + * awsCISRepos: string[] + * enterpriseRepos: string[] + * }} + */ +export const getRepos = (): { + awsCISRepos: string[] + enterpriseRepos: string[] +} => { + try { + const { awsCISRepos, enterpriseRepos } = require("ts-commons/lib/repo-sets") + return { + awsCISRepos, + enterpriseRepos, + } + } catch (e) { + console.log("ts-commons package is NOT available...stubbing out repos.") + + return { + awsCISRepos: [], + enterpriseRepos: [], + } + } +} diff --git a/yarn.lock b/yarn.lock index 5539e45f5..62a58ffa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11185,6 +11185,10 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +ts-commons@gruntwork-io/ts-commons#v1.0.0: + version "1.0.0" + resolved "git+ssh://git@github.com/gruntwork-io/ts-commons.git#1a56fec327ea602b4472663599c63ec6118b7f01" + ts-essentials@^2.0.3: version "2.0.12" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745"