Skip to content

Commit

Permalink
[CORE-624] Notify users when clicking enterprise subscription links (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
oredavids authored Mar 17, 2023
1 parent 5b3fd15 commit 48d4a5d
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 64 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ build/
# IntelliJ files
.idea
*.iml

# VS Code files
.vscode
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"yargs": "^17.4.0"
},
"optionalDependencies": {
"docs-sourcer": "git+ssh://[email protected]/gruntwork-io/docs-sourcer.git#v0.2"
"docs-sourcer": "git+ssh://[email protected]/gruntwork-io/docs-sourcer.git#v0.2",
"ts-commons": "gruntwork-io/ts-commons#v1.0.0"
},
"browserslist": {
"production": [
Expand Down
1 change: 1 addition & 0 deletions src/components/.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.module.css"
42 changes: 23 additions & 19 deletions src/components/SubscribersOnlyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ 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<SubscribersOnlyModalProps> = ({
externalLink,
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
Expand All @@ -26,15 +29,18 @@ export const SubscribersOnlyModal: React.FC<SubscribersOnlyModalProps> = ({
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()
Expand All @@ -51,35 +57,33 @@ export const SubscribersOnlyModal: React.FC<SubscribersOnlyModalProps> = ({
shouldCloseOnEsc={true}
shouldAcceptOnEnter={false}
shouldCloseOnOverlayClick={true}
handleCancelRequest={handleCancelRequest}
handleAcceptRequest={handleAcceptRequest}
handleCancelRequest={clearLink}
handleAcceptRequest={clearLink}
>
<h2>
{subscriberType
? `For ${subscriberType} Subscribers Only`
: "For Subscribers Only"}
</h2>
<p>
This link leads to the private{" "}
{gitHubRepoName && gitHubRepoName.length >= 1 && (
<code>{gitHubRepoName[1]}</code>
)}{" "}
repository visible only to subscribers; everyone else will see a 404.
This link leads to the private <code>{repoName}</code> repository
visible only to {subscriberType && `${subscriberType} `}
subscribers; everyone else will see a 404.
</p>
<div>
<input type="checkbox" onClick={setDontWarnMe} />
<label>Don't warn me again</label>
</div>
<div className={styles.buttonsContainer}>
<a onClick={(e) => onRequestClose(e)} href="#">
<a onClick={onRequestClose} href="#">
Cancel
</a>
<a
href={externalLink}
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
Expand Down
136 changes: 92 additions & 44 deletions src/theme/Root.js
Original file line number Diff line number Diff line change
@@ -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 <Root> 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",
Expand Down Expand Up @@ -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) => {
Expand All @@ -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 <a> we're attempting to extract the
// href data from, and so we search for the closest parent <a>. In the event that
// an <a> is clicked directly, that <a> 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) {
Expand All @@ -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
}

Expand All @@ -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
}
}
Expand All @@ -160,29 +213,24 @@ function Root({ children }) {
return (
<>
<SubscribersOnlyModal
showModal={displaySubscriberNotice}
showModal={!!subscriberNoticeLink}
externalLink={subscriberNoticeLink}
localStorageKey={DONT_SHOW_PRIVATE_GITHUB_WARNING_KEY}
handleCancelRequest={() => {
setDisplaySubscriberNotice(false)
setSubscriberNoticeLink("")
}}
handleAcceptRequest={() => {
setDisplaySubscriberNotice(false)
}}
clearLink={() => setSubscriberNoticeLink("")}
/>
<SubscribersOnlyModal
showModal={displayCisNotice}
showModal={!!cisNoticeLink}
externalLink={cisNoticeLink}
localStorageKey={DONT_SHOW_CIS_GITHUB_WARNING_KEY}
subscriberType="CIS"
handleCancelRequest={() => {
setDisplayCisNotice(false)
setCisNoticeLink("")
}}
handleAcceptRequest={() => {
setDisplayCisNotice(false)
}}
clearLink={() => setCisNoticeLink("")}
/>
<SubscribersOnlyModal
showModal={!!enterpriseNoticeLink}
externalLink={enterpriseNoticeLink}
localStorageKey={DONT_SHOW_ENTERPRISE_GITHUB_WARNING_KEY}
subscriberType="Enterprise"
clearLink={() => setEnterpriseNoticeLink("")}
/>
{children}
</>
Expand Down
1 change: 1 addition & 0 deletions utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ts-commons"
31 changes: 31 additions & 0 deletions utils/ts-commons.ts
Original file line number Diff line number Diff line change
@@ -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: [],
}
}
}
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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://[email protected]/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"
Expand Down

0 comments on commit 48d4a5d

Please sign in to comment.