diff --git a/package.json b/package.json index 55a8b59..275f364 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@pluralsight/ps-design-system-button": "^24.1.1", "@pluralsight/ps-design-system-carousel": "^14.1.1", "@pluralsight/ps-design-system-core": "^10.0.4", + "@pluralsight/ps-design-system-datawell": "^7.0.11", + "@pluralsight/ps-design-system-dialog": "^15.0.12", "@pluralsight/ps-design-system-dropdown": "^13.1.1", "@pluralsight/ps-design-system-emptystate": "^14.1.1", "@pluralsight/ps-design-system-icon": "^25.4.0", @@ -32,6 +34,7 @@ "@pluralsight/ps-design-system-navitem": "^6.1.1", "@pluralsight/ps-design-system-navuser": "^5.0.16", "@pluralsight/ps-design-system-normalize": "^7.0.4", + "@pluralsight/ps-design-system-position": "^9.1.2", "@pluralsight/ps-design-system-table": "^17.1.1", "@pluralsight/ps-design-system-text": "^20.1.27", "@pluralsight/ps-design-system-textinput": "^12.1.1", @@ -48,7 +51,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-github-btn": "^1.3.0", - "react-table": "^7.7.0" + "react-table": "^7.7.0", + "use-http": "^1.0.27" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", diff --git a/patches/use-http+1.0.27.patch b/patches/use-http+1.0.27.patch new file mode 100644 index 0000000..96b8f71 --- /dev/null +++ b/patches/use-http+1.0.27.patch @@ -0,0 +1,10 @@ +diff --git a/node_modules/use-http/dist/cjs/types.d.ts b/node_modules/use-http/dist/cjs/types.d.ts +index e33cb72..37a643e 100644 +--- a/node_modules/use-http/dist/cjs/types.d.ts ++++ b/node_modules/use-http/dist/cjs/types.d.ts +@@ -215,4 +215,5 @@ export declare type NonObjectKeysOf = { + }[keyof T]; + export declare type ObjectValuesOf> = Exclude, object>, never>, Array>; + export declare type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; ++//@ts-ignore + export declare type Flatten = Pick> & UnionToIntersection>; diff --git a/src/App.tsx b/src/App.tsx index 324989c..6fb791a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,7 @@ const WebhookList = React.lazy( () => import("./WebhookDisplay/WebhookList.component") ); import { ApolloProvider } from "@apollo/client"; -import { WebhookStoreUrlContext } from "./WebhookStoreUrl/WebhookStoreUrl.context"; +import { WebhookStoreUrlContext } from "./NavBar/WebhookStoreUrl/WebhookStoreUrl.context"; import { createApolloClient } from "./apollo.client"; import { useStateInLocalStorage } from "./use-state-with-local-storage.hook"; import { isValidHttpUrl } from "./utils/is-valid-url"; diff --git a/src/ProxyStatus/HelpButton.component.tsx b/src/NavBar/ProxyStatus/HelpButton.component.tsx similarity index 93% rename from src/ProxyStatus/HelpButton.component.tsx rename to src/NavBar/ProxyStatus/HelpButton.component.tsx index 2ff2d51..512dd68 100644 --- a/src/ProxyStatus/HelpButton.component.tsx +++ b/src/NavBar/ProxyStatus/HelpButton.component.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useRef } from "react"; -import helpIcon from "../animations/help.json"; +import helpIcon from "../../animations/help.json"; import lottie from "lottie-web"; import { Label } from "@pluralsight/ps-design-system-text"; -import "../animations/animation-container.css"; +import "../../animations/animation-container.css"; const animationName = "help"; diff --git a/src/ProxyStatus/ProxyStatus.component.tsx b/src/NavBar/ProxyStatus/ProxyStatus.component.tsx similarity index 100% rename from src/ProxyStatus/ProxyStatus.component.tsx rename to src/NavBar/ProxyStatus/ProxyStatus.component.tsx diff --git a/src/ProxyStatus/Pulser.component.tsx b/src/NavBar/ProxyStatus/Pulser.component.tsx similarity index 100% rename from src/ProxyStatus/Pulser.component.tsx rename to src/NavBar/ProxyStatus/Pulser.component.tsx diff --git a/src/ProxyStatus/pulser.css b/src/NavBar/ProxyStatus/pulser.css similarity index 100% rename from src/ProxyStatus/pulser.css rename to src/NavBar/ProxyStatus/pulser.css diff --git a/src/NavBar/StoreConfig/StoreConfigDialog.tsx b/src/NavBar/StoreConfig/StoreConfigDialog.tsx new file mode 100644 index 0000000..710051c --- /dev/null +++ b/src/NavBar/StoreConfig/StoreConfigDialog.tsx @@ -0,0 +1,79 @@ +import DataWell from "@pluralsight/ps-design-system-datawell"; +import Link from "@pluralsight/ps-design-system-link/dist/esm/react"; +import { Heading, List, P } from "@pluralsight/ps-design-system-text"; +import React from "react"; + +export const StoreConfigInnerDialog = ({ + availableStores, + storageLimit, + defaultTargets, + accessConfig, + userHasAccessToStore, +}: { + availableStores: { url: string; display: string }[]; + accessConfig: { type: "public" | "private"; sublabel: string }; + storageLimit?: number; + defaultTargets?: string[]; + userHasAccessToStore: boolean; +}) => { + return ( + <> + +

Store Config

+
+
+ + {accessConfig.type} {accessConfig.type === "public" ? "⚠️" : null} + + {storageLimit && ( + {storageLimit} + )} + {defaultTargets && ( + + + {defaultTargets.map((target) => ( + {target} + ))} + + + )} +
+ + {userHasAccessToStore + ? "You have access to this store" + : "You don't have access to this store"} + + +

Your private webhooks stores

+
+ + + {availableStores.length > 0 + ? availableStores.map((store) => ( + + + {store.display} + + + )) + : null} + + +

+ + + WebhookStore access documentation + + +

+ +

+ + + You don't see your organisation here? + + +

+ + ); +}; diff --git a/src/NavBar/StoreConfig/StoreConfigNavItem.tsx b/src/NavBar/StoreConfig/StoreConfigNavItem.tsx new file mode 100644 index 0000000..a3a6410 --- /dev/null +++ b/src/NavBar/StoreConfig/StoreConfigNavItem.tsx @@ -0,0 +1,140 @@ +import { Label } from "@pluralsight/ps-design-system-text"; +import { Below } from "@pluralsight/ps-design-system-position"; + +import React, { useContext, useEffect, useState } from "react"; +import Button from "@pluralsight/ps-design-system-button"; +import { StoreConfigInnerDialog } from "./StoreConfigDialog"; +import Dialog from "@pluralsight/ps-design-system-dialog"; +import useFetch from "use-http"; +import { WebhookStoreUrlContext } from "../WebhookStoreUrl/WebhookStoreUrl.context"; +import { ACCESS_TOKEN_KEY, IDENTITY_TOKEN_KEY } from "../../local-storage"; +import { decodeJWT } from "../../utils/decode-jwt"; + +export type AuthMetadata = + | { protected: true; protectionRule: "hostname webhook.store" } + | { protected: true; protectionRule: "github-org"; ghOrg: string } + | { protected: false }; + +export const StoreConfigNavItem = () => { + const [isClicked, setClicked] = useState(false); + + const [authConfig, setAuthConfig] = useState({ + protected: false, + }); + const [storeConfig, setStoreConfig] = useState<{ + maxNumberOfWebhookPerHost?: number; + defaultTarget?: string[]; + userHasAccessToStore: boolean; + }>({ userHasAccessToStore: false }); + + const { value: webhookStoreUrl } = useContext(WebhookStoreUrlContext); + const accessToken = localStorage.getItem(ACCESS_TOKEN_KEY); + const { get, response } = useFetch(webhookStoreUrl, { + headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}, + }); + const idToken = localStorage.getItem(IDENTITY_TOKEN_KEY); + const identityToken = + idToken && + decodeJWT<{ name: string; ghOrganisations: string[] }, any>(idToken); + + useEffect(() => { + getConfigs(); + }, []); + + async function getConfigs() { + const initialiseAuthConfig = await get("auth-metadata"); + if (response.ok) setAuthConfig(initialiseAuthConfig); + const initialiseStoreConfig = await get("store-metadata"); + if (response.ok) + setStoreConfig({ ...initialiseStoreConfig, userHasAccessToStore: true }); + } + + const accessConfig = describeAccessFromAuthConfig( + authConfig, + webhookStoreUrl + ); + const availableStores = identityToken + ? [ + { + url: `https://${identityToken.payload.name}.github-org.webhook.store/?access_token=${idToken}`, + display: `${identityToken.payload.name}.github-org.webhook.store`, + }, + ...identityToken.payload.ghOrganisations.map((orgName) => ({ + url: `https://${orgName}.github.webhook.store/?access_token=${idToken}`, + display: `${orgName}.github-org.webhook.store`, + })), + ] + : [ + { + url: "https://github.webhook.store", + display: "github.webhook.store", + }, + ]; + const defaultTargets = storeConfig.defaultTarget; + const storageLimit = storeConfig.maxNumberOfWebhookPerHost; + + return ( + + + + } + when={isClicked} + > + + + ); +}; + +const describeAccessFromAuthConfig = ( + authConfig: AuthMetadata, + webhookStoreUrl: string +): { type: "public" | "private"; sublabel: string } => { + if (!authConfig.protected) { + return { type: "public", sublabel: "Anyone with the link" }; + } + + if (authConfig.protectionRule === "github-org") { + return { + type: "private", + sublabel: `Only members of ${authConfig.ghOrg} on GitHub`, + }; + } + + if (authConfig.protectionRule === "hostname webhook.store") { + const webhookStoreDomain = new URL(webhookStoreUrl).hostname; + if (webhookStoreDomain.endsWith(".github.webhook.store")) { + const githubUserName = + webhookStoreDomain.split(".")[webhookStoreDomain.split(".").length - 4]; + return { + type: "private", + sublabel: `Only Github user ${githubUserName}`, + }; + } + if (webhookStoreDomain.endsWith(".github-org.webhook.store")) { + const githubOrgaName = + webhookStoreDomain.split(".")[webhookStoreDomain.split(".").length - 4]; + return { + type: "private", + sublabel: `Only members of ${githubOrgaName} on GitHub`, + }; + } + return { type: "public", sublabel: "Anyone with the link" }; + } + + return { type: "public", sublabel: "Anyone with the link" }; +}; diff --git a/src/User/LoginOrDisplayUser.tsx b/src/NavBar/User/LoginOrDisplayUser.tsx similarity index 91% rename from src/User/LoginOrDisplayUser.tsx rename to src/NavBar/User/LoginOrDisplayUser.tsx index d3c7202..7892a2c 100644 --- a/src/User/LoginOrDisplayUser.tsx +++ b/src/NavBar/User/LoginOrDisplayUser.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from "react"; import NavUser from "@pluralsight/ps-design-system-navuser"; -import { decodeJWT } from "../utils/decode-jwt"; -import { ACCESS_TOKEN_KEY, IDENTITY_TOKEN_KEY } from "../local-storage"; +import { decodeJWT } from "../../utils/decode-jwt"; +import { ACCESS_TOKEN_KEY, IDENTITY_TOKEN_KEY } from "../../local-storage"; const getIdentityTokenFromStorageAndCleanUrl = (): string | null => { const storedIdentityToken = localStorage.getItem(IDENTITY_TOKEN_KEY); diff --git a/src/WebhookStoreUrl/WebhookStoreUrl.component.tsx b/src/NavBar/WebhookStoreUrl/WebhookStoreUrl.component.tsx similarity index 93% rename from src/WebhookStoreUrl/WebhookStoreUrl.component.tsx rename to src/NavBar/WebhookStoreUrl/WebhookStoreUrl.component.tsx index a3fa39f..b13a303 100644 --- a/src/WebhookStoreUrl/WebhookStoreUrl.component.tsx +++ b/src/NavBar/WebhookStoreUrl/WebhookStoreUrl.component.tsx @@ -19,7 +19,7 @@ export function WebhookStoreUrlInput() { size={TextInput.sizes.small} defaultValue={value} onBlur={(event) => { - setValue(event.target.value); + setValue(new URL(event.target.value).origin); }} > diff --git a/src/WebhookStoreUrl/WebhookStoreUrl.context.ts b/src/NavBar/WebhookStoreUrl/WebhookStoreUrl.context.ts similarity index 100% rename from src/WebhookStoreUrl/WebhookStoreUrl.context.ts rename to src/NavBar/WebhookStoreUrl/WebhookStoreUrl.context.ts diff --git a/src/TopNav.tsx b/src/TopNav.tsx index 0be9ed5..ab07548 100644 --- a/src/TopNav.tsx +++ b/src/TopNav.tsx @@ -1,11 +1,12 @@ import NavBar from "@pluralsight/ps-design-system-navbar"; import NavBrand from "@pluralsight/ps-design-system-navbrand"; import React from "react"; -import { WebhookStoreUrlInput } from "./WebhookStoreUrl/WebhookStoreUrl.component"; +import { WebhookStoreUrlInput } from "./NavBar/WebhookStoreUrl/WebhookStoreUrl.component"; import GitHubButton from "react-github-btn"; import NavItem from "@pluralsight/ps-design-system-navitem"; -import { ProxyStatus } from "./ProxyStatus/ProxyStatus.component"; -import { LoginOrDisplayUser } from "./User/LoginOrDisplayUser"; +import { ProxyStatus } from "./NavBar/ProxyStatus/ProxyStatus.component"; +import { LoginOrDisplayUser } from "./NavBar/User/LoginOrDisplayUser"; +import { StoreConfigNavItem } from "./NavBar/StoreConfig/StoreConfigNavItem"; function SkillsLogo() { return ( @@ -73,6 +74,7 @@ export default function TopNav() { , , + , ]} utility={ diff --git a/src/WebhookDisplay/WebhookList.component.tsx b/src/WebhookDisplay/WebhookList.component.tsx index 6227def..89d5f8b 100644 --- a/src/WebhookDisplay/WebhookList.component.tsx +++ b/src/WebhookDisplay/WebhookList.component.tsx @@ -11,7 +11,7 @@ import { } from "react-table"; import { forwardWebhookToLocalhost } from "../forward-to-localhost"; import posthog from "posthog-js"; -import { WebhookStoreUrlContext } from "../WebhookStoreUrl/WebhookStoreUrl.context"; +import { WebhookStoreUrlContext } from "../NavBar/WebhookStoreUrl/WebhookStoreUrl.context"; import { UpdateQueryFn } from "@apollo/client/core/watchQueryOptions"; const largePayloadCellStyle: React.CSSProperties = { diff --git a/src/apollo.client.ts b/src/apollo.client.ts index 2632aaa..878d7d6 100644 --- a/src/apollo.client.ts +++ b/src/apollo.client.ts @@ -77,6 +77,10 @@ const getAccessToken = async ( } ); const json = await accessTokenRequest.json(); + if (json.statusCode > 300) { + console.error("Cannot refresh token", json); + throw new Error(json.message); + } const accessToken = json.accessToken; localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); diff --git a/yarn.lock b/yarn.lock index 37c9543..851e99a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1618,6 +1618,24 @@ dependencies: "@pluralsight/ps-design-system-util" "^10.1.3" +"@pluralsight/ps-design-system-datawell@^7.0.11": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@pluralsight/ps-design-system-datawell/-/ps-design-system-datawell-7.0.11.tgz#e87793720e24d753ab1082e65422e476275e7836" + integrity sha512-GNt5v7MjM+QGdDNqL90tNgUW6t8nbom7k/EhzrpdNrZiBfVsmIGj9h7YAEJtQWmRjJq8ZePsYfRvqZkK/wQqtw== + dependencies: + "@pluralsight/ps-design-system-core" "^10.0.4" + "@pluralsight/ps-design-system-text" "^20.1.27" + "@pluralsight/ps-design-system-util" "^10.1.3" + +"@pluralsight/ps-design-system-dialog@^15.0.12": + version "15.0.12" + resolved "https://registry.yarnpkg.com/@pluralsight/ps-design-system-dialog/-/ps-design-system-dialog-15.0.12.tgz#c4c99e584c253e1d727c279d53ff607891a74d06" + integrity sha512-siAtI4IagHYDii9bYCjVTre856pxHTP6dAQCR7CDTbph7kOUalYHK62ZZSOtl2xJzAqW59ZzeBsCthCp+XjRYQ== + dependencies: + "@pluralsight/ps-design-system-core" "^10.0.4" + "@pluralsight/ps-design-system-focusmanager" "^8.0.10" + "@pluralsight/ps-design-system-util" "^10.1.3" + "@pluralsight/ps-design-system-dropdown@^13.1.1": version "13.1.3" resolved "https://registry.yarnpkg.com/@pluralsight/ps-design-system-dropdown/-/ps-design-system-dropdown-13.1.3.tgz#1a6840f76c353f46039175d5c2baf948aeac357e" @@ -1638,6 +1656,11 @@ "@pluralsight/ps-design-system-core" "^10.0.4" "@pluralsight/ps-design-system-util" "^10.1.3" +"@pluralsight/ps-design-system-focusmanager@^8.0.10": + version "8.0.10" + resolved "https://registry.yarnpkg.com/@pluralsight/ps-design-system-focusmanager/-/ps-design-system-focusmanager-8.0.10.tgz#0cee32013f22857299fe09a0f129099949cc2d09" + integrity sha512-jYikS3wwytE1WK5Th7oLq7NyvE6/W1Kfsg1394lJk7O1VVQne22nXmAWFUukfyp+CxFfxF/V2knmFoIsDhoSHg== + "@pluralsight/ps-design-system-halo@^12.0.8": version "12.0.8" resolved "https://registry.yarnpkg.com/@pluralsight/ps-design-system-halo/-/ps-design-system-halo-12.0.8.tgz#1e6cd43f679e7dd1ede5799546f8ac776941858d" @@ -1732,6 +1755,14 @@ "@pluralsight/ps-design-system-core" "^10.0.4" normalize.css "^8.0.1" +"@pluralsight/ps-design-system-position@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@pluralsight/ps-design-system-position/-/ps-design-system-position-9.1.2.tgz#4a30e12336cd3adbeb819599f67031bae28c7a96" + integrity sha512-6GuvdDyhTWRvr2lSuztwSbtNuQIvMsHig3+LMXH8q491Rh9QJQub8r53IDQF1ljVJMyDxX+rfwX4CbJtUDom0g== + dependencies: + "@pluralsight/ps-design-system-core" "^10.0.4" + "@pluralsight/ps-design-system-util" "^10.1.3" + "@pluralsight/ps-design-system-screenreaderonly@^5.0.11": version "5.0.11" resolved "https://registry.yarnpkg.com/@pluralsight/ps-design-system-screenreaderonly/-/ps-design-system-screenreaderonly-5.0.11.tgz#b3c06a878b35350e75e8e455eaa072473fd67a48" @@ -11291,6 +11322,25 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +urs@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/urs/-/urs-0.0.8.tgz#8a0e0b792073cdb7eec926d08ab1e017ffab3f66" + integrity sha512-LaSSPpr91XrVA3vW2zPupw4K6DSQEDKdL4yQZX1mO2fpljIMpB5zctrjRvxLurelWSgKsHsCmfHNCImscryirQ== + +use-http@^1.0.27: + version "1.0.27" + resolved "https://registry.yarnpkg.com/use-http/-/use-http-1.0.27.tgz#f0acdecebf7dd7f9f3218dc83a68239658b5d452" + integrity sha512-R2V3dzkx+YfIi3Sm35njGFRBzPEyM1AODMx6VSyHiyYzVVq2iYbA3HEjGK5fyw66D8stK5iaP4zU3X7LDmuiyg== + dependencies: + urs "^0.0.8" + use-ssr "^1.0.24" + utility-types "^3.10.0" + +use-ssr@^1.0.24: + version "1.0.25" + resolved "https://registry.yarnpkg.com/use-ssr/-/use-ssr-1.0.25.tgz#c7f54b59d6e52db26749b1d4115a650101a190bd" + integrity sha512-VYF8kJKI+X7+U4XgGoUER2BUl0vIr+8OhlIhyldgSGE0KHMoDRXPvWeHUUeUktq7ACEOVLzXGq1+QRxcvtwvyQ== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -11328,6 +11378,11 @@ utila@~0.4: resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== +utility-types@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"