Skip to content

Commit 48d4a5d

Browse files
authored
[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
1 parent 5b3fd15 commit 48d4a5d

File tree

8 files changed

+157
-64
lines changed

8 files changed

+157
-64
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,6 @@ build/
8181
# IntelliJ files
8282
.idea
8383
*.iml
84+
85+
# VS Code files
86+
.vscode

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"yargs": "^17.4.0"
5454
},
5555
"optionalDependencies": {
56-
"docs-sourcer": "git+ssh://[email protected]/gruntwork-io/docs-sourcer.git#v0.2"
56+
"docs-sourcer": "git+ssh://[email protected]/gruntwork-io/docs-sourcer.git#v0.2",
57+
"ts-commons": "gruntwork-io/ts-commons#v1.0.0"
5758
},
5859
"browserslist": {
5960
"production": [

src/components/.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module "*.module.css"

src/components/SubscribersOnlyModal.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@ import React from "react"
22
import { Modal } from "./Modal"
33
import styles from "./Modal.module.css"
44

5+
export const gruntworkGithubOrg = "https://github.com/gruntwork-io/"
6+
7+
/** @type {RegExp} Match a link prefixed by the gruntworkGithubOrg and capture the next path reference */
8+
export const repoNamePattern = new RegExp(`^${gruntworkGithubOrg}(.*?)(\/|$)`)
9+
510
interface SubscribersOnlyModalProps {
611
externalLink: string
712
localStorageKey: string
813
subscriberType?: string
914
showModal: boolean
10-
handleCancelRequest: () => void
11-
handleAcceptRequest?: () => void
15+
clearLink: () => void
1216
}
1317

1418
export const SubscribersOnlyModal: React.FC<SubscribersOnlyModalProps> = ({
1519
externalLink,
1620
localStorageKey,
1721
subscriberType,
1822
showModal,
19-
handleCancelRequest,
20-
handleAcceptRequest,
23+
clearLink,
2124
}) => {
2225
const onRequestClose = (e) => {
2326
// 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<SubscribersOnlyModalProps> = ({
2629
window.localStorage.removeItem(localStorageKey)
2730
}
2831

29-
handleCancelRequest()
32+
clearLink()
33+
e.preventDefault() // prevent the browser from handling a Cancel button click and scrolling to top
34+
}
3035

31-
// prevent the browser from handling a Cancel button click and scrolling to top
32-
e.preventDefault()
36+
const repoNameMatchArray: RegExpMatchArray | null =
37+
externalLink.match(repoNamePattern)
38+
39+
if (!repoNameMatchArray) {
40+
return <></> // The link is not a Gruntwork Github repo link
3341
}
3442

35-
const gitHubRepoName = externalLink.match(
36-
/https:\/\/github.com\/gruntwork-io\/([^/]*)/
37-
)
43+
const repoName = repoNameMatchArray[1]
3844

3945
const setDontWarnMe = (event) => {
4046
event.stopPropagation()
@@ -51,35 +57,33 @@ export const SubscribersOnlyModal: React.FC<SubscribersOnlyModalProps> = ({
5157
shouldCloseOnEsc={true}
5258
shouldAcceptOnEnter={false}
5359
shouldCloseOnOverlayClick={true}
54-
handleCancelRequest={handleCancelRequest}
55-
handleAcceptRequest={handleAcceptRequest}
60+
handleCancelRequest={clearLink}
61+
handleAcceptRequest={clearLink}
5662
>
5763
<h2>
5864
{subscriberType
5965
? `For ${subscriberType} Subscribers Only`
6066
: "For Subscribers Only"}
6167
</h2>
6268
<p>
63-
This link leads to the private{" "}
64-
{gitHubRepoName && gitHubRepoName.length >= 1 && (
65-
<code>{gitHubRepoName[1]}</code>
66-
)}{" "}
67-
repository visible only to subscribers; everyone else will see a 404.
69+
This link leads to the private <code>{repoName}</code> repository
70+
visible only to {subscriberType && `${subscriberType} `}
71+
subscribers; everyone else will see a 404.
6872
</p>
6973
<div>
7074
<input type="checkbox" onClick={setDontWarnMe} />
7175
<label>Don't warn me again</label>
7276
</div>
7377
<div className={styles.buttonsContainer}>
74-
<a onClick={(e) => onRequestClose(e)} href="#">
78+
<a onClick={onRequestClose} href="#">
7579
Cancel
7680
</a>
7781
<a
7882
href={externalLink}
7983
target="_blank"
8084
data-modal-exempt={true}
8185
onClick={() => {
82-
handleAcceptRequest()
86+
setTimeout(clearLink, 500) // Wait .5seconds to allow propagation to external link before clearing the link from state
8387
}}
8488
>
8589
Continue to GitHub

src/theme/Root.js

Lines changed: 92 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import React, { useState, useEffect } from "react"
2-
import { SubscribersOnlyModal } from "/src/components/SubscribersOnlyModal.tsx"
3-
4-
const gruntworkGithubOrg = "https://github.com/gruntwork-io/"
1+
/**
2+
* This file is the mechanism for adding stateful logic to a docusaurus
3+
* application since the <Root> component is rendered at the very top of the
4+
* React tree and never unmounts.
5+
* We swizzle(customize) the Root component by creating this file: Root.js
6+
* https://docusaurus.io/docs/swizzling#wrapper-your-site-with-root
7+
*/
58

6-
const gruntworkCisRepoName = "terraform-aws-cis-service-catalog"
9+
import React, { useState, useEffect } from "react"
10+
import { getRepos } from "/utils"
11+
import {
12+
SubscribersOnlyModal,
13+
repoNamePattern,
14+
gruntworkGithubOrg,
15+
} from "/src/components/SubscribersOnlyModal.tsx"
716

817
const publicGruntworkRepoNames = [
918
"bash-commons",
@@ -53,29 +62,46 @@ const publicGruntworkRepoNames = [
5362
"website-comments",
5463
]
5564

65+
const { awsCISRepos, enterpriseRepos } = getRepos()
66+
5667
/**
57-
* Checks if a link references a known public Gruntwork repo
68+
* Checks if a given list of repo names includes a repo name extracted from a given url that matches the repoNamePattern
5869
*
59-
* @param string url
70+
* @param {string[]} repoNames
71+
* @param {string} url
6072
* @return {boolean}
6173
*/
62-
const isPublicGruntworkRepo = (url) => {
74+
const listIncludesRepo = (repoNames, url) => {
6375
if (!url) {
6476
return false
6577
}
66-
// Match a link prefixed by the gruntworkGithubOrg and capture the next path reference
67-
const pattern = new RegExp(`^${gruntworkGithubOrg}(.*?)(\/|$)`)
68-
// e.g for a given link https://github.com/gruntwork-io/docs/intro -> `docs`
69-
const repoName = url.match(pattern)[1]
78+
79+
const repoMatchArray = url.match(repoNamePattern)
80+
if (!repoMatchArray) {
81+
return false
82+
}
83+
84+
const repoName = repoMatchArray[1] // e.g for a given link https://github.com/gruntwork-io/docs/intro -> `docs`
7085

7186
// returns boolean
72-
return publicGruntworkRepoNames.includes(repoName)
87+
return repoNames.includes(repoName)
88+
}
89+
90+
/**
91+
* Checks if a link references a known public Gruntwork repo
92+
*
93+
* @param {string} url
94+
* @return {boolean}
95+
*/
96+
const isPublicGruntworkRepo = (url) => {
97+
// returns boolean
98+
return listIncludesRepo(publicGruntworkRepoNames, url)
7399
}
74100

75101
/**
76102
* Checks if a link references a private Gruntwork repo
77103
*
78-
* @param string url
104+
* @param {string} url
79105
* @return {boolean}
80106
*/
81107
const isPrivateGruntworkRepo = (url) => {
@@ -85,33 +111,49 @@ const isPrivateGruntworkRepo = (url) => {
85111
}
86112

87113
/**
88-
* Checks if a link references the Gruntwork CIS service catalog repo
114+
* Checks if a link references a Gruntwork CIS repo
89115
*
90-
* @param string url
116+
* @param {string} url
91117
* @return {boolean}
92118
*/
93-
94119
const isGruntworkCisRepo = (url) => {
95-
return url && url.startsWith(`${gruntworkGithubOrg}${gruntworkCisRepoName}`)
120+
// awsCISRepos is an array of strings, e.g. `gruntwork-io/cis-docs`
121+
const cisRepoNames = awsCISRepos.map((repo) => repo.split("/")[1])
122+
return listIncludesRepo(cisRepoNames, url)
123+
}
124+
125+
/**
126+
* Checks if a link references a Gruntwork Enterprise repo
127+
*
128+
* @param {string} url
129+
* @return {boolean}
130+
*/
131+
const isGruntworkEnterpriseRepo = (url) => {
132+
// enterpriseRepos is an array of strings, e.g. `gruntwork-io/enterprise-docs`
133+
const enterpriseRepoNames = enterpriseRepos.map((repo) => repo.split("/")[1])
134+
return listIncludesRepo(enterpriseRepoNames, url)
96135
}
97136

98137
export const DONT_SHOW_PRIVATE_GITHUB_WARNING_KEY = "dontWarnGitHubLinks"
99138
export const DONT_SHOW_CIS_GITHUB_WARNING_KEY = "dontWarnCISLinks"
139+
export const DONT_SHOW_ENTERPRISE_GITHUB_WARNING_KEY = "dontWarnEnterpriseLinks"
100140

101141
function Root({ children }) {
102-
const [displaySubscriberNotice, setDisplaySubscriberNotice] = useState(false)
103142
const [subscriberNoticeLink, setSubscriberNoticeLink] = useState("")
104-
105-
const [displayCisNotice, setDisplayCisNotice] = useState(false)
106143
const [cisNoticeLink, setCisNoticeLink] = useState("")
144+
const [enterpriseNoticeLink, setEnterpriseNoticeLink] = useState("")
107145

108-
useEffect(() => {
146+
useEffect(function showModalForPrivateGithubLinks() {
109147
const listener = (event) => {
110148
// Sometimes our links wrap components, such as Cards. In these cases, the event
111149
// target is often a child element of the <a> we're attempting to extract the
112150
// href data from, and so we search for the closest parent <a>. In the event that
113151
// an <a> is clicked directly, that <a> itself will be returned.
114-
const targetLink = event.target.closest("a")
152+
const targetLink = event?.target?.closest("a")
153+
154+
if (!targetLink || !targetLink.href) {
155+
return
156+
}
115157

116158
// Allow clicks on the external GitHub link FROM the modal notices to work normally
117159
if (targetLink.dataset.modalExempt) {
@@ -124,13 +166,25 @@ function Root({ children }) {
124166
)
125167

126168
if (dontWarn) {
127-
setDisplayCisNotice(false)
128169
return
129170
}
130171

131-
event.preventDefault()
172+
event.preventDefault() // This prevents the link from opening & ensures the modal is displayed
132173
setCisNoticeLink(targetLink.href)
133-
setDisplayCisNotice(true)
174+
return
175+
}
176+
177+
if (isGruntworkEnterpriseRepo(targetLink.href)) {
178+
const dontWarn = window.localStorage.getItem(
179+
DONT_SHOW_ENTERPRISE_GITHUB_WARNING_KEY
180+
)
181+
182+
if (dontWarn) {
183+
return
184+
}
185+
186+
event.preventDefault() // This prevents the link from opening & ensures the modal is displayed
187+
setEnterpriseNoticeLink(targetLink.href)
134188
return
135189
}
136190

@@ -144,9 +198,8 @@ function Root({ children }) {
144198
return
145199
}
146200

147-
event.preventDefault()
201+
event.preventDefault() // This prevents the link from opening & ensures the modal is displayed
148202
setSubscriberNoticeLink(targetLink.href)
149-
setDisplaySubscriberNotice(true)
150203
return
151204
}
152205
}
@@ -160,29 +213,24 @@ function Root({ children }) {
160213
return (
161214
<>
162215
<SubscribersOnlyModal
163-
showModal={displaySubscriberNotice}
216+
showModal={!!subscriberNoticeLink}
164217
externalLink={subscriberNoticeLink}
165218
localStorageKey={DONT_SHOW_PRIVATE_GITHUB_WARNING_KEY}
166-
handleCancelRequest={() => {
167-
setDisplaySubscriberNotice(false)
168-
setSubscriberNoticeLink("")
169-
}}
170-
handleAcceptRequest={() => {
171-
setDisplaySubscriberNotice(false)
172-
}}
219+
clearLink={() => setSubscriberNoticeLink("")}
173220
/>
174221
<SubscribersOnlyModal
175-
showModal={displayCisNotice}
222+
showModal={!!cisNoticeLink}
176223
externalLink={cisNoticeLink}
177224
localStorageKey={DONT_SHOW_CIS_GITHUB_WARNING_KEY}
178225
subscriberType="CIS"
179-
handleCancelRequest={() => {
180-
setDisplayCisNotice(false)
181-
setCisNoticeLink("")
182-
}}
183-
handleAcceptRequest={() => {
184-
setDisplayCisNotice(false)
185-
}}
226+
clearLink={() => setCisNoticeLink("")}
227+
/>
228+
<SubscribersOnlyModal
229+
showModal={!!enterpriseNoticeLink}
230+
externalLink={enterpriseNoticeLink}
231+
localStorageKey={DONT_SHOW_ENTERPRISE_GITHUB_WARNING_KEY}
232+
subscriberType="Enterprise"
233+
clearLink={() => setEnterpriseNoticeLink("")}
186234
/>
187235
{children}
188236
</>

utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./ts-commons"

utils/ts-commons.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Get repos from ts-commons package if available. Otherwise return empty arrays.
3+
*
4+
* ts-commons package is a private package so some users may not have access to
5+
* it. Use this function to safely pull in an optional package to avoid both
6+
* build-time and runtime errors.
7+
*
8+
* @return {*} {{
9+
* awsCISRepos: string[]
10+
* enterpriseRepos: string[]
11+
* }}
12+
*/
13+
export const getRepos = (): {
14+
awsCISRepos: string[]
15+
enterpriseRepos: string[]
16+
} => {
17+
try {
18+
const { awsCISRepos, enterpriseRepos } = require("ts-commons/lib/repo-sets")
19+
return {
20+
awsCISRepos,
21+
enterpriseRepos,
22+
}
23+
} catch (e) {
24+
console.log("ts-commons package is NOT available...stubbing out repos.")
25+
26+
return {
27+
awsCISRepos: [],
28+
enterpriseRepos: [],
29+
}
30+
}
31+
}

yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11185,6 +11185,10 @@ trough@^1.0.0:
1118511185
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
1118611186
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
1118711187

11188+
ts-commons@gruntwork-io/ts-commons#v1.0.0:
11189+
version "1.0.0"
11190+
resolved "git+ssh://[email protected]/gruntwork-io/ts-commons.git#1a56fec327ea602b4472663599c63ec6118b7f01"
11191+
1118811192
ts-essentials@^2.0.3:
1118911193
version "2.0.12"
1119011194
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745"

0 commit comments

Comments
 (0)