From 6bc52f8276f4b8cfd720c09c72ac854900030600 Mon Sep 17 00:00:00 2001 From: Adrian Machado Date: Wed, 9 Aug 2023 13:48:32 -0400 Subject: [PATCH 01/50] Dark mode #21 * Dark mode support * Remove log * Update CSS for no keys * Fix theme usage * Fix all styles to use regular css --- examples/nextjs/package.json | 2 + .../nextjs/src/components/Authenticating.tsx | 4 +- examples/nextjs/src/components/Header.tsx | 8 ++- examples/nextjs/src/components/KeyManager.tsx | 5 +- examples/nextjs/src/components/Layout.tsx | 13 +++-- .../nextjs/src/components/ThemePicker.tsx | 23 ++++++++ examples/nextjs/src/components/Toggle.tsx | 58 +++++++++++++++++++ examples/nextjs/src/contexts/ThemeContext.tsx | 36 ++++++++++++ examples/nextjs/src/pages/index.tsx | 4 +- examples/nextjs/tailwind.config.js | 1 + package-lock.json | 25 ++++++++ .../src/components/ApiKeyManager.module.css | 4 ++ .../react/src/components/ApiKeyManager.tsx | 52 ++++++++++++----- .../src/components/ConsumerControl.module.css | 34 ++++++++--- .../react/src/components/ConsumerControl.tsx | 4 +- .../src/components/ConsumerLoading.module.css | 14 ++++- .../src/components/KeyControl.module.css | 14 ++++- packages/react/src/components/KeyControl.tsx | 7 +-- .../src/components/SimpleMenu.module.css | 12 +++- packages/react/tailwind.config.js | 15 ++++- 20 files changed, 286 insertions(+), 49 deletions(-) create mode 100644 examples/nextjs/src/components/ThemePicker.tsx create mode 100644 examples/nextjs/src/components/Toggle.tsx create mode 100644 examples/nextjs/src/contexts/ThemeContext.tsx diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 092ee48..696651b 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@headlessui/react": "^1.7.16", + "@heroicons/react": "^2.0.18", "@auth0/auth0-react": "2.2.0", "@types/node": "20.4.2", "@types/react": "18.2.14", diff --git a/examples/nextjs/src/components/Authenticating.tsx b/examples/nextjs/src/components/Authenticating.tsx index 11c6d0b..b24a1f0 100644 --- a/examples/nextjs/src/components/Authenticating.tsx +++ b/examples/nextjs/src/components/Authenticating.tsx @@ -2,8 +2,8 @@ import Spinner from "./Spinner"; export default function Authenticating() { return ( -
-
+
+
{" "} Authenticating... diff --git a/examples/nextjs/src/components/Header.tsx b/examples/nextjs/src/components/Header.tsx index d882896..3ab14cd 100644 --- a/examples/nextjs/src/components/Header.tsx +++ b/examples/nextjs/src/components/Header.tsx @@ -1,5 +1,6 @@ /* eslint-disable @next/next/no-img-element */ import { useAuth0 } from "@auth0/auth0-react"; +import ThemePicker from "./ThemePicker"; function Header() { const { isAuthenticated, loginWithRedirect, logout } = useAuth0(); @@ -21,12 +22,13 @@ function Header() { alt="api key manager logo" className="h-10 w-10" /> -

API Key Manager

+

API Key Manager

-
+
+ diff --git a/examples/nextjs/src/components/KeyManager.tsx b/examples/nextjs/src/components/KeyManager.tsx index 4e5b64d..54fd9a2 100644 --- a/examples/nextjs/src/components/KeyManager.tsx +++ b/examples/nextjs/src/components/KeyManager.tsx @@ -2,8 +2,9 @@ import ApiKeyManager, { Consumer, DefaultApiKeyManagerProvider, } from "@zuplo/react-api-key-manager"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useContext, useMemo, useState } from "react"; import Spinner from "./Spinner"; +import { ThemeContext } from "@/contexts/ThemeContext"; interface Props { apiUrl: string; @@ -11,6 +12,7 @@ interface Props { } export default function KeyManager({ apiUrl, accessToken }: Props) { + const [theme] = useContext(ThemeContext); const [isCreating, setIsCreating] = useState(false); const [showIsLoading, setShowIsLoading] = useState(false); @@ -74,6 +76,7 @@ export default function KeyManager({ apiUrl, accessToken }: Props) { provider={provider} menuItems={menuItems} showIsLoading={showIsLoading} + theme={theme} />
+ +
+
+
+
{isLoading ? : children}
+
+
+
); } diff --git a/examples/nextjs/src/components/ThemePicker.tsx b/examples/nextjs/src/components/ThemePicker.tsx new file mode 100644 index 0000000..fc9db62 --- /dev/null +++ b/examples/nextjs/src/components/ThemePicker.tsx @@ -0,0 +1,23 @@ +"use client"; +import { useContext } from "react"; +import Toggle from "./Toggle"; +import { ThemeContext } from "@/contexts/ThemeContext"; +import { MoonIcon, SunIcon } from "@heroicons/react/24/outline"; + +export default function ThemePicker() { + const [theme, setTheme] = useContext(ThemeContext); + const handleThemeChange = (useDarkTheme: boolean) => { + setTheme(useDarkTheme ? "dark" : "light"); + }; + + return ( + } + disabledIcon={} + /> + ); +} diff --git a/examples/nextjs/src/components/Toggle.tsx b/examples/nextjs/src/components/Toggle.tsx new file mode 100644 index 0000000..db66a99 --- /dev/null +++ b/examples/nextjs/src/components/Toggle.tsx @@ -0,0 +1,58 @@ +"use client"; +import { Switch } from "@headlessui/react"; + +type ToggleProps = { + enabledIcon: JSX.Element; + disabledIcon: JSX.Element; + enabledBackgroundStyle: string; + disabledBackgroundStyle: string; + isEnabled: boolean; + onChange: (isEnabled: boolean) => void; +}; + +export default function Toggle({ + enabledIcon, + disabledIcon, + enabledBackgroundStyle, + disabledBackgroundStyle, + isEnabled, + onChange, +}: ToggleProps) { + return ( + + Use setting + + + + + + ); +} diff --git a/examples/nextjs/src/contexts/ThemeContext.tsx b/examples/nextjs/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000..a3d94d9 --- /dev/null +++ b/examples/nextjs/src/contexts/ThemeContext.tsx @@ -0,0 +1,36 @@ +"use client"; +import React, { PropsWithChildren, useEffect, useState } from "react"; + +const getSystemDefaultThemePreference = (): "dark" | "light" => { + if ( + typeof window !== "undefined" && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + return "dark"; + } + return "light"; +}; + +export const ThemeContext = React.createContext< + ["dark" | "light", (theme: "dark" | "light") => void] +>([ + getSystemDefaultThemePreference(), + () => { + return; + }, +]); + +const ThemeProvider = ({ children }: PropsWithChildren) => { + const [theme, setTheme] = useState<"dark" | "light">("light"); + useEffect(() => { + setTheme(getSystemDefaultThemePreference()); + }, []); + + return ( + +
{children}
+
+ ); +}; + +export default ThemeProvider; diff --git a/examples/nextjs/src/pages/index.tsx b/examples/nextjs/src/pages/index.tsx index 6fe3d32..9642535 100644 --- a/examples/nextjs/src/pages/index.tsx +++ b/examples/nextjs/src/pages/index.tsx @@ -15,7 +15,9 @@ export default function Home() { return ( -
Login to continue ↗️
+
+ Login to continue ↗️ +
); } diff --git a/examples/nextjs/tailwind.config.js b/examples/nextjs/tailwind.config.js index b47a403..5b536f0 100644 --- a/examples/nextjs/tailwind.config.js +++ b/examples/nextjs/tailwind.config.js @@ -16,4 +16,5 @@ module.exports = { }, }, plugins: [], + darkMode: "class", }; diff --git a/package-lock.json b/package-lock.json index 89b1b4e..da6d1a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,8 @@ "name": "dev-console-key-api", "version": "1.3.0", "dependencies": { + "@headlessui/react": "^1.7.16", + "@heroicons/react": "^2.0.18", "@auth0/auth0-react": "2.2.0", "@types/node": "20.4.2", "@types/react": "18.2.14", @@ -605,6 +607,29 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@headlessui/react": { + "version": "1.7.16", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.16.tgz", + "integrity": "sha512-2MphIAZdSUacZBT6EXk8AJkj+EuvaaJbtCyHTJrPsz8inhzCl7qeNPI1uk1AUvCgWylVtdN8cVVmnhUDPxPy3g==", + "dependencies": { + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@heroicons/react": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.18.tgz", + "integrity": "sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", diff --git a/packages/react/src/components/ApiKeyManager.module.css b/packages/react/src/components/ApiKeyManager.module.css index 15413f3..70e7810 100644 --- a/packages/react/src/components/ApiKeyManager.module.css +++ b/packages/react/src/components/ApiKeyManager.module.css @@ -21,3 +21,7 @@ .query-error-heading-text { @apply font-bold; } + +.no-keys-message { + @apply py-4 dark:text-white; +} diff --git a/packages/react/src/components/ApiKeyManager.tsx b/packages/react/src/components/ApiKeyManager.tsx index c92d866..b6fcad1 100644 --- a/packages/react/src/components/ApiKeyManager.tsx +++ b/packages/react/src/components/ApiKeyManager.tsx @@ -9,14 +9,27 @@ import { useEffect } from "react"; interface Props { menuItems?: MenuItem[]; + theme?: "light" | "dark"; provider: ApiKeyManagerProvider; showIsLoading?: boolean; } -function ApiKeyManager({ provider, menuItems, showIsLoading }: Props) { +const getSystemDefaultThemePreference = (): "dark" | "light" => { + if ( + typeof window !== "undefined" && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + return "dark"; + } + return "light"; +}; + +function ApiKeyManager({ provider, menuItems, showIsLoading, theme }: Props) { const queryEngine = useProviderQueryEngine(provider); const query = queryEngine.useMyConsumersQuery(); - + const themeStyle = `zp-key-manager--${ + theme ?? getSystemDefaultThemePreference() + }`; useEffect(() => { const handle = provider.registerOnRefresh(() => { queryEngine.refreshMyConsumers(); @@ -27,7 +40,11 @@ function ApiKeyManager({ provider, menuItems, showIsLoading }: Props) { }, [provider, queryEngine]); if (!query.data && query.isLoading) { - return ; + return ( +
+ +
+ ); } if (query.error) { @@ -49,23 +66,28 @@ function ApiKeyManager({ provider, menuItems, showIsLoading }: Props) { const consumers = query.data?.data; if (!consumers || consumers.length === 0) { - return
You have no API keys
; + return ( +
+
You have no API keys
+
+ ); } const loading = query.isLoading || showIsLoading === true ? true : false; - return ( - {consumers.map((c) => { - return ( - - ); - })} +
+ {consumers.map((c) => { + return ( + + ); + })} +
); } diff --git a/packages/react/src/components/ConsumerControl.module.css b/packages/react/src/components/ConsumerControl.module.css index 08471f1..752916c 100644 --- a/packages/react/src/components/ConsumerControl.module.css +++ b/packages/react/src/components/ConsumerControl.module.css @@ -1,10 +1,14 @@ /* ConsumerControl.module.css */ .consumer-control-container { - @apply rounded-lg bg-slate-50 border-zinc-200 border mb-5; + @apply rounded-lg bg-slate-50 border-zinc-200 dark:border-none border mb-5 selection:dark:text-zinc-900 selection:dark:bg-white; +} + +:is(.dark .consumer-control-container) { + background-color: #4f566b; } .consumer-control-header { - @apply flex flex-row justify-between border-b border-zinc-200 items-center; + @apply flex flex-row justify-between border-b border-zinc-200 dark:border-none items-center; } .consumer-control-input-container { @@ -12,11 +16,19 @@ } .consumer-control-input { - @apply flex-1 disabled:opacity-50 rounded border border-gray-400 m-1 ml-2 my-2 px-2 bg-white w-full py-1; + @apply flex-1 disabled:opacity-50 rounded border border-slate-400 m-1 ml-2 my-2 px-2 bg-white dark:bg-slate-600 dark:text-white ring-0 w-full py-1; } .consumer-control-button { - @apply hover:bg-slate-300 text-zinc-700 px-2 bg-slate-200 rounded flex flex-row items-center text-sm m-1 my-2 h-8; + @apply hover:bg-slate-300 hover:dark:bg-slate-400 text-zinc-700 dark:text-white px-2 bg-slate-200 dark:bg-slate-500 rounded flex flex-row items-center text-sm m-1 my-2 h-8; +} + +.consumer-control-menu-spinner-container { + @apply h-full pb-[10px] pt-1 mr-1; +} + +.consumer-control-menu-spinner { + @apply h-5 w-5 animate-spin dark:text-white; } .consumer-control-spinner-container { @@ -24,7 +36,7 @@ } .consumer-control-spinner { - @apply h-4 w-4 animate-spin; + @apply h-4 w-4 animate-spin dark:text-white; } .consumer-control-save-icon { @@ -36,7 +48,7 @@ } .consumer-control-description { - @apply ml-4 text-zinc-900; + @apply ml-4 text-zinc-900 dark:text-white; } .consumer-menu-button-wrapper { @@ -47,8 +59,12 @@ @apply hover:bg-slate-200 rounded p-1; } +.dark .consumer-control-menu-button:hover { + background-color: #697386; +} + .consumer-control-menu-icon { - @apply h-5 w-5 text-zinc-500; + @apply h-5 w-5 text-zinc-500 dark:text-white; } .consumer-control-error-container { @@ -87,6 +103,10 @@ @apply bg-white rounded-b-lg p-4; } +.dark .consumer-control-content { + background-color: #2a2f45; +} + .consumer-control-key-control { @apply mb-3; } diff --git a/packages/react/src/components/ConsumerControl.tsx b/packages/react/src/components/ConsumerControl.tsx index 85212a7..34da08f 100644 --- a/packages/react/src/components/ConsumerControl.tsx +++ b/packages/react/src/components/ConsumerControl.tsx @@ -156,8 +156,8 @@ const ConsumerControl = ({ )}
{isLoading || keyRollMutation.isLoading ? ( -
- +
+
) : ( diff --git a/packages/react/src/components/ConsumerLoading.module.css b/packages/react/src/components/ConsumerLoading.module.css index 02edd4b..2315d99 100644 --- a/packages/react/src/components/ConsumerLoading.module.css +++ b/packages/react/src/components/ConsumerLoading.module.css @@ -1,9 +1,13 @@ .consumer-loading-container { - @apply rounded-lg bg-slate-50 border-zinc-200 border mb-5; + @apply rounded-lg bg-slate-50 border-zinc-200 dark:border-none border mb-5; +} + +.dark .consumer-loading-container { + background-color: #4f566b; } .consumer-loading-header { - @apply flex flex-row border-b border-zinc-200 justify-between items-center; + @apply flex flex-row border-b border-zinc-200 dark:border-none justify-between items-center; } .consumer-loading-pulse { @@ -15,13 +19,17 @@ } .consumer-loading-ellipsis-icon { - @apply h-5 w-5; + @apply h-5 w-5 dark:text-white; } .consumer-loading-content { @apply bg-white flex flex-col rounded-b-lg p-4 py-2; } +.dark .consumer-loading-content { + background-color: #2a2f45; +} + .key-loading-container { @apply flex w-full justify-between; } diff --git a/packages/react/src/components/KeyControl.module.css b/packages/react/src/components/KeyControl.module.css index 28d7eb0..fb8b0c4 100644 --- a/packages/react/src/components/KeyControl.module.css +++ b/packages/react/src/components/KeyControl.module.css @@ -4,15 +4,23 @@ } .key-control-key { - @apply font-mono text-ellipsis overflow-hidden py-2 mr-2 text-zinc-800; + @apply font-mono text-ellipsis overflow-hidden py-2 mr-2 text-zinc-800 dark:text-zinc-200; } .key-control-buttons { - @apply flex gap-x-1 justify-end text-zinc-500; + @apply flex gap-x-1 justify-end text-zinc-500 dark:text-zinc-200; } .key-control-button { - @apply rounded p-1 hover:bg-slate-200; + @apply rounded p-1; +} + +.key-control-button:hover { + @apply bg-slate-200; +} + +.dark .key-control-button:hover { + background-color: #3c4257; } .key-control-button-active { diff --git a/packages/react/src/components/KeyControl.tsx b/packages/react/src/components/KeyControl.tsx index 911eea0..ec2c495 100644 --- a/packages/react/src/components/KeyControl.tsx +++ b/packages/react/src/components/KeyControl.tsx @@ -57,11 +57,8 @@ const KeyControl = ({ onMutationComplete(deleteKeyMutation.error); // We use the isLoading flag here to reset the error state whenever the // mutation is triggered - }, [ - deleteKeyMutation.error, - deleteKeyMutation.isLoading, - onMutationComplete, - ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [deleteKeyMutation.isLoading]); function handleDeleteKey() { deleteKeyMutation.mutate({ diff --git a/packages/react/src/components/SimpleMenu.module.css b/packages/react/src/components/SimpleMenu.module.css index 0172611..ca71775 100644 --- a/packages/react/src/components/SimpleMenu.module.css +++ b/packages/react/src/components/SimpleMenu.module.css @@ -12,10 +12,18 @@ @apply absolute top-0 right-0 z-50 shadow-md rounded bg-white; } +.dark .simple-menu-dialog { + background-color: #1a1f36; +} + .simple-menu-item-button { - @apply whitespace-nowrap rounded w-full hover:bg-slate-50 px-3 py-1 text-right; + @apply whitespace-nowrap rounded w-full hover:bg-slate-50 hover:dark:text-white px-3 py-1 text-right; +} + +.dark .simple-menu-item-button:hover { + background-color: #2a2f45; } .simple-menu-dropdown { - @apply text-zinc-800 p-1; + @apply text-zinc-800 dark:text-zinc-200 p-1; } diff --git a/packages/react/tailwind.config.js b/packages/react/tailwind.config.js index 1777e6d..9329cb8 100644 --- a/packages/react/tailwind.config.js +++ b/packages/react/tailwind.config.js @@ -2,7 +2,20 @@ export default { content: ["./src/**/*.{js,ts,jsx,tsx}"], theme: { - extend: {}, + extend: { + colors: { + // Do NOT use this for colors, as a bug in the tailwind css export will + // prevent the tailwind.css from picking these up + // Instead use selectors and values directly + // Ex. for dark mode use + /** + * .dark .simple-menu-dialog { + * background-color: #1a1f36; + * } + */ + }, + }, }, plugins: [], + darkMode: "class", }; From 83ce3d9aa4256b9c818aa8b2967935047eddf79b Mon Sep 17 00:00:00 2001 From: Integration Service Date: Wed, 9 Aug 2023 17:55:03 +0000 Subject: [PATCH 02/50] 1.4.0 --- examples/nextjs/package.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- packages/react/package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 696651b..ace53f3 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "dev-console-key-api", - "version": "1.3.0", + "version": "1.4.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package-lock.json b/package-lock.json index da6d1a8..830d6f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zuplo/api-key-manager", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zuplo/api-key-manager", - "version": "1.3.0", + "version": "1.4.0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -73,11 +73,11 @@ }, "examples/nextjs": { "name": "dev-console-key-api", - "version": "1.3.0", + "version": "1.4.0", "dependencies": { + "@auth0/auth0-react": "2.2.0", "@headlessui/react": "^1.7.16", "@heroicons/react": "^2.0.18", - "@auth0/auth0-react": "2.2.0", "@types/node": "20.4.2", "@types/react": "18.2.14", "@types/react-dom": "18.2.7", @@ -6283,7 +6283,7 @@ }, "packages/react": { "name": "@zuplo/react-api-key-manager", - "version": "1.3.0", + "version": "1.4.0", "license": "MIT", "devDependencies": { "@types/node": "^20.3.1", diff --git a/package.json b/package.json index f69d56c..04b036e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/api-key-manager", - "version": "1.3.0", + "version": "1.4.0", "description": "A React component to manage API keys", "license": "MIT", "author": "Zuplo", diff --git a/packages/react/package.json b/packages/react/package.json index c844684..21a0e45 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/react-api-key-manager", - "version": "1.3.0", + "version": "1.4.0", "description": "A React component to manage API keys", "keywords": [ "react", From 80efd9566a57514d15e4159881e4760b87cf4ef8 Mon Sep 17 00:00:00 2001 From: Adrian Machado Date: Wed, 9 Aug 2023 22:32:20 -0400 Subject: [PATCH 03/50] Fix backgrounds in themes #25 --- examples/nextjs/.gitignore | 1 + .../react/src/components/ApiKeyManager.tsx | 27 ++++++++++++++----- .../src/components/ConsumerControl.module.css | 12 ++------- .../src/components/ConsumerLoading.module.css | 12 ++------- .../src/components/SimpleMenu.module.css | 6 +---- packages/react/tailwind.config.js | 2 +- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore index 80161be..9826d7d 100644 --- a/examples/nextjs/.gitignore +++ b/examples/nextjs/.gitignore @@ -31,3 +31,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.env.local diff --git a/packages/react/src/components/ApiKeyManager.tsx b/packages/react/src/components/ApiKeyManager.tsx index b6fcad1..4d79177 100644 --- a/packages/react/src/components/ApiKeyManager.tsx +++ b/packages/react/src/components/ApiKeyManager.tsx @@ -7,14 +7,19 @@ import { XCircleIcon } from "./icons"; import styles from "./ApiKeyManager.module.css"; import { useEffect } from "react"; +type ThemeOptions = "light" | "dark" | "system"; +type Theme = "light" | "dark"; interface Props { menuItems?: MenuItem[]; - theme?: "light" | "dark"; + /** + * @default "light" + */ + theme?: ThemeOptions; provider: ApiKeyManagerProvider; showIsLoading?: boolean; } -const getSystemDefaultThemePreference = (): "dark" | "light" => { +const getSystemDefaultThemePreference = (): Theme => { if ( typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches @@ -24,12 +29,22 @@ const getSystemDefaultThemePreference = (): "dark" | "light" => { return "light"; }; -function ApiKeyManager({ provider, menuItems, showIsLoading, theme }: Props) { +const getTheme = (theme: ThemeOptions): Theme => { + if (theme === "system") { + return getSystemDefaultThemePreference(); + } + return theme; +}; + +function ApiKeyManager({ + provider, + menuItems, + showIsLoading, + theme = "light", +}: Props) { const queryEngine = useProviderQueryEngine(provider); const query = queryEngine.useMyConsumersQuery(); - const themeStyle = `zp-key-manager--${ - theme ?? getSystemDefaultThemePreference() - }`; + const themeStyle = `zp-key-manager--${getTheme(theme)}`; useEffect(() => { const handle = provider.registerOnRefresh(() => { queryEngine.refreshMyConsumers(); diff --git a/packages/react/src/components/ConsumerControl.module.css b/packages/react/src/components/ConsumerControl.module.css index 752916c..bdee2fc 100644 --- a/packages/react/src/components/ConsumerControl.module.css +++ b/packages/react/src/components/ConsumerControl.module.css @@ -1,10 +1,6 @@ /* ConsumerControl.module.css */ .consumer-control-container { - @apply rounded-lg bg-slate-50 border-zinc-200 dark:border-none border mb-5 selection:dark:text-zinc-900 selection:dark:bg-white; -} - -:is(.dark .consumer-control-container) { - background-color: #4f566b; + @apply rounded-lg bg-slate-50 dark:bg-[#4f566b] border-zinc-200 dark:border-none border mb-5 selection:dark:text-zinc-900 selection:dark:bg-white; } .consumer-control-header { @@ -100,11 +96,7 @@ } .consumer-control-content { - @apply bg-white rounded-b-lg p-4; -} - -.dark .consumer-control-content { - background-color: #2a2f45; + @apply bg-white rounded-b-lg p-4 dark:bg-[#2a2f45]; } .consumer-control-key-control { diff --git a/packages/react/src/components/ConsumerLoading.module.css b/packages/react/src/components/ConsumerLoading.module.css index 2315d99..ea6fc49 100644 --- a/packages/react/src/components/ConsumerLoading.module.css +++ b/packages/react/src/components/ConsumerLoading.module.css @@ -1,9 +1,5 @@ .consumer-loading-container { - @apply rounded-lg bg-slate-50 border-zinc-200 dark:border-none border mb-5; -} - -.dark .consumer-loading-container { - background-color: #4f566b; + @apply rounded-lg bg-slate-50 dark:bg-[#4f566b] border-zinc-200 dark:border-none border mb-5; } .consumer-loading-header { @@ -23,11 +19,7 @@ } .consumer-loading-content { - @apply bg-white flex flex-col rounded-b-lg p-4 py-2; -} - -.dark .consumer-loading-content { - background-color: #2a2f45; + @apply bg-white dark:bg-[#2a2f45] flex flex-col rounded-b-lg p-4 py-2; } .key-loading-container { diff --git a/packages/react/src/components/SimpleMenu.module.css b/packages/react/src/components/SimpleMenu.module.css index ca71775..175848c 100644 --- a/packages/react/src/components/SimpleMenu.module.css +++ b/packages/react/src/components/SimpleMenu.module.css @@ -9,11 +9,7 @@ } .simple-menu-dialog { - @apply absolute top-0 right-0 z-50 shadow-md rounded bg-white; -} - -.dark .simple-menu-dialog { - background-color: #1a1f36; + @apply absolute top-0 right-0 z-50 shadow-md rounded bg-white dark:bg-[#1a1f36]; } .simple-menu-item-button { diff --git a/packages/react/tailwind.config.js b/packages/react/tailwind.config.js index 9329cb8..a2f5fc8 100644 --- a/packages/react/tailwind.config.js +++ b/packages/react/tailwind.config.js @@ -9,7 +9,7 @@ export default { // Instead use selectors and values directly // Ex. for dark mode use /** - * .dark .simple-menu-dialog { + * .dark .simple-menu-dialog:hover { * background-color: #1a1f36; * } */ From fa6d2ae7e2c2f37ddf996bc2c095666431c79d69 Mon Sep 17 00:00:00 2001 From: Integration Service Date: Thu, 10 Aug 2023 02:36:29 +0000 Subject: [PATCH 04/50] 1.5.0 --- examples/nextjs/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- packages/react/package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index ace53f3..69e36ff 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "dev-console-key-api", - "version": "1.4.0", + "version": "1.5.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package-lock.json b/package-lock.json index 830d6f2..e0c2b9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zuplo/api-key-manager", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zuplo/api-key-manager", - "version": "1.4.0", + "version": "1.5.0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -73,7 +73,7 @@ }, "examples/nextjs": { "name": "dev-console-key-api", - "version": "1.4.0", + "version": "1.5.0", "dependencies": { "@auth0/auth0-react": "2.2.0", "@headlessui/react": "^1.7.16", @@ -6283,7 +6283,7 @@ }, "packages/react": { "name": "@zuplo/react-api-key-manager", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "devDependencies": { "@types/node": "^20.3.1", diff --git a/package.json b/package.json index 04b036e..06a6df0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/api-key-manager", - "version": "1.4.0", + "version": "1.5.0", "description": "A React component to manage API keys", "license": "MIT", "author": "Zuplo", diff --git a/packages/react/package.json b/packages/react/package.json index 21a0e45..909d3f3 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/react-api-key-manager", - "version": "1.4.0", + "version": "1.5.0", "description": "A React component to manage API keys", "keywords": [ "react", From eff0a602233056a004c1f6e32e9c7e3f88b2dece Mon Sep 17 00:00:00 2001 From: aabedraba Date: Thu, 10 Aug 2023 14:04:31 +0400 Subject: [PATCH 05/50] add README instruction to example --- examples/nextjs/README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md index 1dfa229..e91cb6b 100644 --- a/examples/nextjs/README.md +++ b/examples/nextjs/README.md @@ -2,13 +2,34 @@ This sample shows how to create your own Developer Console that uses the Zuplo API Key Service (https://dev.zuplo.com/docs) to help your developers manage their own API keys. -## Unfinished, still need +## Features +- Auth0 Login +- Zuplo API Key Manager - Light and dark mode -- Consumer but no key UI -# Fast Follows +## Run locally + +1. Clone the repo + +``` +npx create-next-app --example \ + https://github.com/zuplo/api-key-manager/tree/main/examples/nextjs +``` + +2. Add `NEXT_PUBLIC_API_URL` value to `.env.local` file + +If you don't have an API Key service, you can use this one: `https://api-key-live-sample-main-21ced70.d2.zuplo.dev`. + +If you want to create your own, you can follow the tutorial from our blogpost [here](https://zuplo.com/blog/2023/08/08/open-source-release). + +3. Run the sample + +``` +npm run dev +``` + +# TODOs - Add delete confirm dialog - Add roll key dialog with option to choose -- Themes From 6bb9a5ffff50c55026adb9201103f6d78ead19f5 Mon Sep 17 00:00:00 2001 From: aabedraba Date: Thu, 10 Aug 2023 14:05:20 +0400 Subject: [PATCH 06/50] update README.md --- examples/nextjs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md index e91cb6b..6456a5a 100644 --- a/examples/nextjs/README.md +++ b/examples/nextjs/README.md @@ -29,7 +29,7 @@ If you want to create your own, you can follow the tutorial from our blogpost [h npm run dev ``` -# TODOs +### Future iteration TODOs - Add delete confirm dialog - Add roll key dialog with option to choose From 03535ae65c54b79b2cec1cdd1717a667f9fe08eb Mon Sep 17 00:00:00 2001 From: Josh Twist Date: Thu, 10 Aug 2023 14:37:19 -0700 Subject: [PATCH 07/50] Initial v2 - much simpler (#24) * initial v2 - much simpler * fixed build * added create and delete * moved most styles, one not working * fixed styling stuff * missing save * removed refresh, should be in separate provider * fixed env.local --- examples/nextjs/src/components/KeyManager.tsx | 81 +---- .../react/src/components/ApiKeyManager.tsx | 89 +++-- .../react/src/components/ConsumerControl.tsx | 328 ++++++++++-------- .../src/components/CreateConsumer.module.css | 28 ++ .../react/src/components/CreateConsumer.tsx | 90 +++++ packages/react/src/components/KeyControl.tsx | 52 +-- .../src/components/SimpleMenu.module.css | 6 +- packages/react/src/components/SimpleMenu.tsx | 14 +- packages/react/src/components/context.ts | 32 ++ packages/react/src/components/icons.tsx | 36 ++ .../src/components/mini-query/async-query.ts | 29 -- .../components/mini-query/useMiniMutation.ts | 47 --- .../src/components/mini-query/useMiniQuery.ts | 41 --- packages/react/src/context.tsx | 6 - packages/react/src/interfaces.ts | 13 +- packages/react/src/useProviderQueryEngine.ts | 101 ------ packages/react/src/useQueryEngineContext.tsx | 10 - 17 files changed, 474 insertions(+), 529 deletions(-) create mode 100644 packages/react/src/components/CreateConsumer.module.css create mode 100644 packages/react/src/components/CreateConsumer.tsx create mode 100644 packages/react/src/components/context.ts delete mode 100644 packages/react/src/components/mini-query/async-query.ts delete mode 100644 packages/react/src/components/mini-query/useMiniMutation.ts delete mode 100644 packages/react/src/components/mini-query/useMiniQuery.ts delete mode 100644 packages/react/src/context.tsx delete mode 100644 packages/react/src/useProviderQueryEngine.ts delete mode 100644 packages/react/src/useQueryEngineContext.tsx diff --git a/examples/nextjs/src/components/KeyManager.tsx b/examples/nextjs/src/components/KeyManager.tsx index 54fd9a2..1a71d51 100644 --- a/examples/nextjs/src/components/KeyManager.tsx +++ b/examples/nextjs/src/components/KeyManager.tsx @@ -1,9 +1,7 @@ import ApiKeyManager, { - Consumer, DefaultApiKeyManagerProvider, } from "@zuplo/react-api-key-manager"; -import { useCallback, useContext, useMemo, useState } from "react"; -import Spinner from "./Spinner"; +import { useContext, useMemo } from "react"; import { ThemeContext } from "@/contexts/ThemeContext"; interface Props { @@ -13,82 +11,17 @@ interface Props { export default function KeyManager({ apiUrl, accessToken }: Props) { const [theme] = useContext(ThemeContext); - const [isCreating, setIsCreating] = useState(false); - const [showIsLoading, setShowIsLoading] = useState(false); const provider = useMemo(() => { return new DefaultApiKeyManagerProvider(apiUrl, accessToken); }, [apiUrl, accessToken]); - const createConsumer = useCallback( - async (description: string) => { - try { - setIsCreating(true); - await provider.createConsumer(description); - provider.refresh(); - } catch (err) { - // TODO - throw err; - } finally { - setIsCreating(false); - } - }, - [provider], - ); - - const deleteConsumer = useCallback( - async (consumerName: string) => { - try { - setShowIsLoading(true); - await provider.deleteConsumer(consumerName); - provider.refresh(); - } catch (err) { - // TODO - throw err; - } finally { - setShowIsLoading(false); - } - }, - [provider], - ); - - function clickCreateConsumer() { - const desc = window.prompt("Enter a description for your new API Key"); - if (desc) { - createConsumer(desc); - } - } - - const menuItems = useMemo(() => { - return [ - { - label: "Delete", - action: async (consumer: Consumer) => { - await deleteConsumer(consumer.name); - }, - }, - ]; - }, [deleteConsumer]); - return ( -
- - -
+ ); } diff --git a/packages/react/src/components/ApiKeyManager.tsx b/packages/react/src/components/ApiKeyManager.tsx index 4d79177..5ddde52 100644 --- a/packages/react/src/components/ApiKeyManager.tsx +++ b/packages/react/src/components/ApiKeyManager.tsx @@ -1,11 +1,11 @@ -import { QueryEngineContext } from "../context"; -import { ApiKeyManagerProvider, MenuItem } from "../interfaces"; -import { useProviderQueryEngine } from "../useProviderQueryEngine"; +import { ApiKeyManagerProvider, DataModel, MenuItem } from "../interfaces"; import ConsumerControl from "./ConsumerControl"; import ConsumerLoading from "./ConsumerLoading"; import { XCircleIcon } from "./icons"; import styles from "./ApiKeyManager.module.css"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { DataContext, ProviderContext } from "./context"; +import CreateConsumer from "./CreateConsumer"; type ThemeOptions = "light" | "dark" | "system"; type Theme = "light" | "dark"; @@ -16,7 +16,8 @@ interface Props { */ theme?: ThemeOptions; provider: ApiKeyManagerProvider; - showIsLoading?: boolean; + enableCreateConsumer?: boolean; + enableDeleteConsumer?: boolean; } const getSystemDefaultThemePreference = (): Theme => { @@ -29,6 +30,11 @@ const getSystemDefaultThemePreference = (): Theme => { return "light"; }; +const DEFAULT_DATA_MODEL: DataModel = { + isFetching: false, + consumers: undefined, +}; + const getTheme = (theme: ThemeOptions): Theme => { if (theme === "system") { return getSystemDefaultThemePreference(); @@ -39,22 +45,31 @@ const getTheme = (theme: ThemeOptions): Theme => { function ApiKeyManager({ provider, menuItems, - showIsLoading, theme = "light", + enableCreateConsumer, + enableDeleteConsumer, }: Props) { - const queryEngine = useProviderQueryEngine(provider); - const query = queryEngine.useMyConsumersQuery(); const themeStyle = `zp-key-manager--${getTheme(theme)}`; + const [dataModel, setDataModel] = useState(DEFAULT_DATA_MODEL); + const [error, setError] = useState(undefined); + + const loadData = async (prov: ApiKeyManagerProvider) => { + try { + setDataModel({ ...dataModel, isFetching: true }); + const result = await prov.getConsumers(); + setDataModel({ consumers: result.data, isFetching: false }); + } catch (err) { + setError((err as Error).message); + console.error(err); + } + }; + useEffect(() => { - const handle = provider.registerOnRefresh(() => { - queryEngine.refreshMyConsumers(); - }); - return () => { - provider.unregisterOnRefresh(handle); - }; - }, [provider, queryEngine]); + void loadData(provider); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [provider]); - if (!query.data && query.isLoading) { + if (!dataModel.consumers && dataModel.isFetching) { return (
@@ -62,7 +77,7 @@ function ApiKeyManager({ ); } - if (query.error) { + if (error) { return (
@@ -71,16 +86,14 @@ function ApiKeyManager({ Error
-
- "{query.error.toString()}" -
+
"{error}"
); } - const consumers = query.data?.data; + const consumers = dataModel.consumers ?? []; - if (!consumers || consumers.length === 0) { + if (consumers.length === 0) { return (
You have no API keys
@@ -88,22 +101,24 @@ function ApiKeyManager({ ); } - const loading = query.isLoading || showIsLoading === true ? true : false; return ( - -
- {consumers.map((c) => { - return ( - - ); - })} -
-
+ + +
+ {consumers.map((c) => { + return ( + + ); + })} +
+ {enableCreateConsumer && } +
+
); } diff --git a/packages/react/src/components/ConsumerControl.tsx b/packages/react/src/components/ConsumerControl.tsx index 34da08f..7d148a6 100644 --- a/packages/react/src/components/ConsumerControl.tsx +++ b/packages/react/src/components/ConsumerControl.tsx @@ -1,84 +1,106 @@ -import { useEffect, useState } from "react"; +import { createContext, useState } from "react"; import { Consumer, MenuItem } from "../interfaces"; -import { useQueryEngineContext } from "../useQueryEngineContext"; import KeyControl from "./KeyControl"; import { SimpleMenu } from "./SimpleMenu"; import { + ArrowPathIcon, EllipsisVerticalIcon, + PencilSquareIcon, Save, Spinner, + TrashIcon, XCircleIcon, XIcon, } from "./icons"; import styles from "./ConsumerControl.module.css"; +import { useDataContext, useProviderContext } from "./context"; interface ConsumerControlProps { consumer: Consumer; menuItems?: MenuItem[]; - isLoading: boolean; + enableDeleteConsumer?: boolean; } +export const ErrorContext = createContext< + [string | undefined, (error: string | undefined) => void] +>([undefined, () => {}]); + +// 7 days +const EXPIRY_PERIOD_MS = 1000 * 60 * 60 * 24 * 7; + const ConsumerControl = ({ consumer, menuItems, - isLoading, + enableDeleteConsumer, }: ConsumerControlProps) => { const [edit, setEdit] = useState(false); - const [queryError, setQueryError] = useState(); + const [error, setError] = useState(); const [description, setDescription] = useState(consumer.description); - const { useConsumerDescriptionMutation, useRollKeyMutation } = - useQueryEngineContext(); - const consumerDescriptionMutation = useConsumerDescriptionMutation(); - const keyRollMutation = useRollKeyMutation(); - - useEffect(() => { - if (consumerDescriptionMutation.error) { - setQueryError(consumerDescriptionMutation.error.toString()); - return; - } - setQueryError(undefined); - }, [ - consumerDescriptionMutation.error, - consumerDescriptionMutation.isLoading, - ]); - - useEffect(() => { - if (keyRollMutation.error) { - setQueryError(keyRollMutation.error.toString()); - return; - } - setQueryError(undefined); - }, [keyRollMutation.error, keyRollMutation.isLoading]); + const [dataModel, setDataModel] = useDataContext(); + const [descriptionUpdating, setDescriptionUpdating] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const provider = useProviderContext(); - const handleMutationComplete = (error: unknown) => { - if (!error) { - setQueryError(undefined); - return; + const handleDescriptionSave = async () => { + try { + setDescriptionUpdating(true); + await provider.updateConsumerDescription(consumer.name, description); + const result = await provider.getConsumers(); + setDataModel({ + isFetching: dataModel?.isFetching, + consumers: result.data, + }); + setEdit(false); + setError(undefined); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + setError(err.message); + } finally { + setDescriptionUpdating(false); } - if (error instanceof Error) { - setQueryError(error.message); - return; - } - console.error("unhandled error", error); }; - const handleLabelSave = async () => { - // We're in edit mode, so change description - await consumerDescriptionMutation.mutate({ - consumerName: consumer.name, - description, - }); - - setEdit(false); + const handleRollKey = async () => { + try { + setIsLoading(true); + await provider.rollKey( + consumer.name, + new Date(Date.now() + EXPIRY_PERIOD_MS), + ); + const result = await provider.getConsumers(); + setDataModel({ + isFetching: dataModel?.isFetching, + consumers: result.data, + }); + setError(undefined); + } catch (err) { + setError((err as Error).message); + } finally { + setIsLoading(false); + } }; - const handleRollKey = () => { - keyRollMutation.mutate({ - consumerName: consumer.name, - // TODO - provide options, expire in 7 days for now - expiresOn: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), - }); + const handleDeleteConsumer = async () => { + try { + if (!provider.deleteConsumer) { + throw new Error( + "Provider does not support deleteConsumer but enableDeleteConsumer is true", + ); + } + setIsLoading(true); + await provider.deleteConsumer(consumer.name); + const result = await provider.getConsumers(); + setDataModel({ + ...dataModel, + consumers: result.data, + }); + setError(undefined); + } catch (err) { + setError((err as Error).message); + } finally { + setIsLoading(false); + } }; const editLabelMenuItem = { @@ -86,125 +108,129 @@ const ConsumerControl = ({ action: () => { setEdit(true); }, + icon: PencilSquareIcon({}), }; - const rollKeysMenuItem = { + const rollKeysMenuItem: MenuItem = { label: consumer.apiKeys.filter((k) => !k.expiresOn).length > 1 ? "Roll keys" : "Roll key", action: handleRollKey, + icon: ArrowPathIcon({}), }; - const fancyDropDownMenuItems = [ - editLabelMenuItem, - rollKeysMenuItem, - ...(menuItems?.map((item) => { - return { - label: item.label, - action: () => { - item.action(consumer); - }, - }; - }) ?? []), - ]; + const deleteConsumerMenuItem: MenuItem = { + label: "Delete", + action: handleDeleteConsumer, + icon: TrashIcon({}), + }; + + const initialMenuItems = [editLabelMenuItem, rollKeysMenuItem]; + + if (enableDeleteConsumer) { + initialMenuItems.push(deleteConsumerMenuItem); + } + + const withCustomMenuItems = [...initialMenuItems, ...(menuItems ?? [])]; return ( -
-
- {edit ? ( -
- event.target.select()} - onKeyUp={(event) => { - event.key === "Enter" && handleLabelSave(); - }} - disabled={consumerDescriptionMutation.isLoading} - type="text" - className={styles["consumer-control-input"]} - onChange={(e) => setDescription(e.target.value)} - defaultValue={consumer.description} - /> - - -
- ) : ( -
- {consumer.description ?? consumer.name} -
- )} -
- {isLoading || keyRollMutation.isLoading ? ( -
- + +
+
+ {edit ? ( +
+ event.target.select()} + onKeyUp={(event) => { + event.key === "Enter" && handleDescriptionSave(); + }} + disabled={descriptionUpdating} + type="text" + className={styles["consumer-control-input"]} + onChange={(e) => setDescription(e.target.value)} + defaultValue={consumer.description} + /> + +
) : ( - -
- -
-
+
+ {consumer.description ?? consumer.name} +
)} +
+ {dataModel.isFetching || isLoading ? ( +
+ +
+ ) : ( + +
+ +
+
+ )} +
-
- {Boolean(queryError) && ( -
-
-
- - - Error - + {Boolean(error) && ( +
+
+
+ + + Error + +
+ +
+
+ "{error}"
- -
-
- "{queryError}"
+ )} +
+ {consumer.apiKeys.map((k) => ( + + ))}
- )} -
- {consumer.apiKeys.map((k) => ( - - ))}
-
+ ); }; diff --git a/packages/react/src/components/CreateConsumer.module.css b/packages/react/src/components/CreateConsumer.module.css new file mode 100644 index 0000000..3a07552 --- /dev/null +++ b/packages/react/src/components/CreateConsumer.module.css @@ -0,0 +1,28 @@ + +.create-consumer-button { + @apply bg-pink-500 rounded hover:bg-pink-700 p-2 text-white flex-none; +} + +.create-consumer-main-button { + @apply text-white rounded bg-pink-500 hover:bg-pink-700 p-2 px-4; +} + +.create-consumer-icon { + @apply w-5 h-auto m-1; +} + +.create-consumer-spinner { + @apply w-5 h-auto m-1 animate-spin; +} + +.create-consumer-label { + @apply rounded shadow-md p-2 flex-grow border border-gray-200 dark:bg-slate-700 dark:border-slate-500 dark:text-white; +} + +.create-consumer-label-invalid { + @apply rounded shadow-md p-2 flex-grow border-2 border-red-500 bg-red-50 dark:border-red-800 dark:bg-red-950 dark:text-white; +} + +.create-consumer-container { + @apply flex flex-row items-center gap-x-2 w-full; +} \ No newline at end of file diff --git a/packages/react/src/components/CreateConsumer.tsx b/packages/react/src/components/CreateConsumer.tsx new file mode 100644 index 0000000..9eba7db --- /dev/null +++ b/packages/react/src/components/CreateConsumer.tsx @@ -0,0 +1,90 @@ +import { useContext, useState } from "react"; +import { ErrorContext } from "./ConsumerControl"; +import { useDataContext, useProviderContext } from "./context"; +import { CheckIcon, Spinner } from "./icons"; +import styles from "./CreateConsumer.module.css"; + +export default function CreateConsumer() { + const provider = useProviderContext(); + const [, setError] = useContext(ErrorContext); + const [dataModel, setDataModel] = useDataContext(); + const [editMode, setEditMode] = useState(false); + const [label, setLabel] = useState(""); + const [isCreating, setIsCreating] = useState(false); + const [isInvalid, setIsInvalid] = useState(false); + + // TODO - handle errors + + async function handleCreateConsumer() { + try { + if (label.trim().length === 0) { + setIsInvalid(true); + return; + } + setIsInvalid(false); + + if (!provider.createConsumer) { + throw new Error( + "Provider does not implement createConsumer but enableCreateConsumer is true", + ); + } + + setIsCreating(true); + await provider.createConsumer(label); + const result = await provider.getConsumers(); + setDataModel({ ...dataModel, consumers: result.data }); + setEditMode(false); + setError(undefined); + } catch (err) { + setError((err as Error).message); + } finally { + setIsCreating(false); + } + } + + function handleLabelChange(e: React.ChangeEvent) { + if (e.target.value.trim().length > 0) { + setIsInvalid(false); + } + setLabel(e.target.value); + } + + if (!editMode) { + return ( + + ); + } + + return ( +
+ + +
+ ); +} diff --git a/packages/react/src/components/KeyControl.tsx b/packages/react/src/components/KeyControl.tsx index ec2c495..12899cc 100644 --- a/packages/react/src/components/KeyControl.tsx +++ b/packages/react/src/components/KeyControl.tsx @@ -1,9 +1,9 @@ import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import relativeTime from "dayjs/plugin/relativeTime"; -import { useEffect, useState } from "react"; +import { useContext, useState } from "react"; import { ApiKey } from "../interfaces"; -import { useQueryEngineContext } from "../useQueryEngineContext"; + import { CheckIcon, DocumentDuplicateIcon, @@ -14,11 +14,12 @@ import { } from "./icons"; import styles from "./KeyControl.module.css"; +import { useDataContext, useProviderContext } from "./context"; +import { ErrorContext } from "./ConsumerControl"; interface KeyControlProps { consumerName: string; apiKey: ApiKey; - onMutationComplete: (error: unknown) => void; } dayjs.extend(relativeTime); @@ -35,15 +36,14 @@ function mask(value: string, mask: boolean) { return maskedPart + lastEightChars; } -const KeyControl = ({ - apiKey, - consumerName, - onMutationComplete, -}: KeyControlProps) => { +const KeyControl = ({ apiKey, consumerName }: KeyControlProps) => { const [masked, setMasked] = useState(true); const [copied, setCopied] = useState(false); - const { useDeleteKeyMutation } = useQueryEngineContext(); - const deleteKeyMutation = useDeleteKeyMutation(); + + const [dataModel, setDataModel] = useDataContext(); + const [keyDeleting, setKeyDeleting] = useState(false); + const [, setError] = useContext(ErrorContext); + const provider = useProviderContext(); function copy(value: string) { navigator.clipboard.writeText(value); @@ -53,22 +53,24 @@ const KeyControl = ({ }, 2000); } - useEffect(() => { - onMutationComplete(deleteKeyMutation.error); - // We use the isLoading flag here to reset the error state whenever the - // mutation is triggered - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [deleteKeyMutation.isLoading]); - - function handleDeleteKey() { - deleteKeyMutation.mutate({ - consumerName: consumerName, - keyId: apiKey.id, - }); + async function handleDeleteKey() { + try { + setKeyDeleting(true); + await provider.deleteKey(consumerName, apiKey.id); + const result = await provider.getConsumers(); + setDataModel({ + isFetching: dataModel.isFetching, + consumers: result.data, + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setError(err.message); + } finally { + setKeyDeleting(false); + } } - const keyMutating = deleteKeyMutation.isLoading; - return (
@@ -106,7 +108,7 @@ const KeyControl = ({ )} {apiKey.expiresOn ? ( - keyMutating ? ( + keyDeleting ? (
diff --git a/packages/react/src/components/SimpleMenu.module.css b/packages/react/src/components/SimpleMenu.module.css index 175848c..1fc4b3a 100644 --- a/packages/react/src/components/SimpleMenu.module.css +++ b/packages/react/src/components/SimpleMenu.module.css @@ -13,7 +13,7 @@ } .simple-menu-item-button { - @apply whitespace-nowrap rounded w-full hover:bg-slate-50 hover:dark:text-white px-3 py-1 text-right; + @apply whitespace-nowrap rounded w-full hover:bg-slate-50 hover:dark:text-white px-3 py-1 text-right flex flex-row items-center; } .dark .simple-menu-item-button:hover { @@ -23,3 +23,7 @@ .simple-menu-dropdown { @apply text-zinc-800 dark:text-zinc-200 p-1; } + +.simple-menu-item-icon { + @apply h-4 w-auto mr-2; +} \ No newline at end of file diff --git a/packages/react/src/components/SimpleMenu.tsx b/packages/react/src/components/SimpleMenu.tsx index bea13b4..a143127 100644 --- a/packages/react/src/components/SimpleMenu.tsx +++ b/packages/react/src/components/SimpleMenu.tsx @@ -1,10 +1,11 @@ -import { useEffect, useRef, useState } from "react"; +import { cloneElement, useEffect, useRef, useState } from "react"; import styles from "./SimpleMenu.module.css"; +import { MenuItem } from ".."; interface Props { disabled?: boolean; - items: { label: string; action: () => void }[]; + items: MenuItem[]; children: JSX.Element; } @@ -52,7 +53,14 @@ export function SimpleMenu({ disabled, items, children }: Props) { onClick={() => click(item.action)} className={styles["simple-menu-item-button"]} > - {item.label} + {item.icon && ( + <> + {cloneElement(item.icon, { + className: styles["simple-menu-item-icon"], + })} + + )} + {item.label} ))}
diff --git a/packages/react/src/components/context.ts b/packages/react/src/components/context.ts new file mode 100644 index 0000000..3d83b44 --- /dev/null +++ b/packages/react/src/components/context.ts @@ -0,0 +1,32 @@ +import { createContext, useContext } from "react"; +import { ApiKeyManagerProvider, DataModel } from "../interfaces"; + +export const DataContext = createContext< + [DataModel | undefined, (dataModel: DataModel) => void] +>([undefined, () => {}]); + +export function useDataContext() { + const ctx = useContext(DataContext); + const [dataModel, setDataModel] = ctx; + if (!dataModel || !setDataModel) { + throw new Error( + `Invalid state, no 'dataModel' or 'setDataModel' available`, + ); + } + return [dataModel, setDataModel] as [ + DataModel, + (dataModel: DataModel) => void, + ]; +} + +export const ProviderContext = createContext( + undefined, +); + +export function useProviderContext() { + const pc = useContext(ProviderContext); + if (!pc) { + throw new Error(`Invalid state, no ProviderContext available`); + } + return pc; +} diff --git a/packages/react/src/components/icons.tsx b/packages/react/src/components/icons.tsx index 321129a..0fcddf3 100644 --- a/packages/react/src/components/icons.tsx +++ b/packages/react/src/components/icons.tsx @@ -207,3 +207,39 @@ export const XIcon = (props: { className?: string }) => ( /> ); + +export const PencilSquareIcon = (props: { className?: string }) => ( + + + +); + +export const ArrowPathIcon = (props: { className?: string }) => ( + + + +); diff --git a/packages/react/src/components/mini-query/async-query.ts b/packages/react/src/components/mini-query/async-query.ts deleted file mode 100644 index 20ffa10..0000000 --- a/packages/react/src/components/mini-query/async-query.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Dispatch, SetStateAction } from "react"; - -export type QueryFn = () => Promise; -export interface CachedQuery { - setData: Dispatch>; - setError: Dispatch>; - setIsLoading: Dispatch>; - queryFn: QueryFn; -} - -export async function asyncQuery({ - setIsLoading, - setError, - setData, - queryFn, -}: // eslint-disable-next-line @typescript-eslint/no-explicit-any -CachedQuery) { - try { - setIsLoading(true); - const data = await queryFn(); - setData(data); - setError(undefined); - setIsLoading(false); - } catch (err) { - setData(undefined); - setError(err); - setIsLoading(false); - } -} diff --git a/packages/react/src/components/mini-query/useMiniMutation.ts b/packages/react/src/components/mini-query/useMiniMutation.ts deleted file mode 100644 index 8102017..0000000 --- a/packages/react/src/components/mini-query/useMiniMutation.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useState } from "react"; -import { asyncQuery } from "./async-query"; -import { cache } from "./useMiniQuery"; - -export interface MutationState { - isLoading: boolean; - wasSuccessful: boolean; - error?: unknown; - mutate: (variables: TVariables) => Promise; -} - -export type MutationFn = (variables: TVariables) => Promise; - -interface MutationOptions { - invalidateQueriesOnSuccess?: string[]; -} - -export function useMiniMutation( - mutationFn: MutationFn, - options?: MutationOptions, -): MutationState { - const [isLoading, setIsLoading] = useState(false); - const [wasSuccessful, setWasSuccessful] = useState(false); - const [error, setError] = useState(); - - const mutate = async (variables: TVariables) => { - try { - setIsLoading(true); - await mutationFn(variables); - setError(undefined); - setWasSuccessful(true); - setIsLoading(false); - options?.invalidateQueriesOnSuccess?.forEach((queryId) => { - const cachedQuery = cache[queryId]; - if (cachedQuery) { - asyncQuery(cachedQuery); - } - }); - } catch (err) { - setError(err); - setWasSuccessful(false); - setIsLoading(false); - } - }; - - return { isLoading, wasSuccessful, error, mutate }; -} diff --git a/packages/react/src/components/mini-query/useMiniQuery.ts b/packages/react/src/components/mini-query/useMiniQuery.ts deleted file mode 100644 index 196714d..0000000 --- a/packages/react/src/components/mini-query/useMiniQuery.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useState } from "react"; -import { asyncQuery, CachedQuery, QueryFn } from "./async-query"; - -export interface QueryState { - isLoading: boolean; - data?: T; - error: unknown; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const cache: Record> = {}; - -export function useMiniQuery( - queryFn: QueryFn, - queryId: string, -): QueryState { - const [isLoading, setIsLoading] = useState(false); - const [data, setData] = useState(); - const [error, setError] = useState(); - - useEffect(() => { - const cachedQuery: CachedQuery = { - setIsLoading, - setError, - setData, - queryFn, - }; - - cache[queryId] = cachedQuery; - void asyncQuery(cachedQuery); - }, [queryId, queryFn]); - - return { isLoading, data, error }; -} - -export async function refreshQuery(queryId: string) { - const cachedQuery = cache[queryId]; - if (cachedQuery) { - await asyncQuery(cachedQuery); - } -} diff --git a/packages/react/src/context.tsx b/packages/react/src/context.tsx deleted file mode 100644 index 551f46b..0000000 --- a/packages/react/src/context.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createContext } from "react"; -import { QueryEngine } from "./useProviderQueryEngine"; - -export const QueryEngineContext = createContext( - undefined, -); diff --git a/packages/react/src/interfaces.ts b/packages/react/src/interfaces.ts index 3151810..56af01f 100644 --- a/packages/react/src/interfaces.ts +++ b/packages/react/src/interfaces.ts @@ -8,14 +8,14 @@ export interface ApiKeyManagerProvider { consumerName: string, description: string, ) => Promise; - refresh: () => void; - registerOnRefresh: (callback: () => void) => RegisterHandle; - unregisterOnRefresh: (handle: RegisterHandle) => void; + createConsumer?: (description: string) => Promise; + deleteConsumer?: (consumerName: string) => Promise; } export interface MenuItem { label: string; - action: (consumer: Consumer) => void; + action: () => void; + icon?: JSX.Element; } export interface ConsumerData { @@ -37,3 +37,8 @@ export interface ApiKey { expiresOn: string | null; key: string; } + +export interface DataModel { + consumers?: Consumer[]; + isFetching: boolean; +} diff --git a/packages/react/src/useProviderQueryEngine.ts b/packages/react/src/useProviderQueryEngine.ts deleted file mode 100644 index fca5830..0000000 --- a/packages/react/src/useProviderQueryEngine.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { useCallback } from "react"; -import { ApiKeyManagerProvider, ConsumerData } from "./interfaces"; -import { - MutationState, - useMiniMutation, -} from "./components/mini-query/useMiniMutation"; -import { - QueryState, - refreshQuery, - useMiniQuery, -} from "./components/mini-query/useMiniQuery"; - -const MY_CONSUMERS_KEY = "my-consumers"; -const INVALIDATE_OPTIONS = { invalidateQueriesOnSuccess: [MY_CONSUMERS_KEY] }; - -interface ConsumerDescriptionMutationOptions { - consumerName: string; - description: string; -} - -interface RollKeyMutationOptions { - consumerName: string; - expiresOn: Date; -} - -interface DeleteKeyMutationOptions { - consumerName: string; - keyId: string; -} - -export interface QueryEngine { - useMyConsumersQuery: () => QueryState; - refreshMyConsumers: () => Promise; - useRollKeyMutation: () => MutationState; - useConsumerDescriptionMutation: () => MutationState; - useDeleteKeyMutation: () => MutationState; -} - -const queryEngineMap = new Map(); - -export function useProviderQueryEngine( - provider: ApiKeyManagerProvider, -): QueryEngine { - const cached = queryEngineMap.get(provider); - - if (cached) { - return cached; - } - - const useMyConsumersQuery = () => { - return useMiniQuery( - useCallback(() => { - return provider.getConsumers(); - }, []), - MY_CONSUMERS_KEY, - ); - }; - - const useRollKeyMutation = () => { - return useMiniMutation( - ({ consumerName, expiresOn }: RollKeyMutationOptions) => { - return provider.rollKey(consumerName, expiresOn); - }, - INVALIDATE_OPTIONS, - ); - }; - - const useConsumerDescriptionMutation = () => { - return useMiniMutation( - ({ consumerName, description }: ConsumerDescriptionMutationOptions) => { - return provider.updateConsumerDescription(consumerName, description); - }, - INVALIDATE_OPTIONS, - ); - }; - - const useDeleteKeyMutation = () => { - return useMiniMutation( - ({ consumerName, keyId }: DeleteKeyMutationOptions) => { - return provider.deleteKey(consumerName, keyId); - }, - INVALIDATE_OPTIONS, - ); - }; - - const refreshMyConsumers = async () => { - refreshQuery(MY_CONSUMERS_KEY); - }; - - const result = { - useMyConsumersQuery, - refreshMyConsumers, - useRollKeyMutation, - useConsumerDescriptionMutation, - useDeleteKeyMutation, - }; - - queryEngineMap.set(provider, result); - - return result; -} diff --git a/packages/react/src/useQueryEngineContext.tsx b/packages/react/src/useQueryEngineContext.tsx deleted file mode 100644 index 958dde2..0000000 --- a/packages/react/src/useQueryEngineContext.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from "react"; -import { QueryEngineContext } from "./context"; - -export function useQueryEngineContext() { - const ctx = useContext(QueryEngineContext); - if (!ctx) { - throw new Error("Invalid State - ControlContext not found"); - } - return ctx; -} From 4f490c37e966da414c27b978833502ae7590520a Mon Sep 17 00:00:00 2001 From: Nathan Totten Date: Thu, 10 Aug 2023 17:38:32 -0400 Subject: [PATCH 08/50] ignore .env files --- .gitignore | 4 ++++ examples/nextjs/.gitignore | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index bd2d7ed..3e56af8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ dist-tailwind dist-ssr .eslintcache +# local env files +.env +.env*.local + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore index 9826d7d..865e58d 100644 --- a/examples/nextjs/.gitignore +++ b/examples/nextjs/.gitignore @@ -25,6 +25,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# local env files +.env +.env*.local + # vercel .vercel From 42163a6c287798f91fd20a49b7d9ab1581b8080b Mon Sep 17 00:00:00 2001 From: Nathan Totten Date: Thu, 10 Aug 2023 17:42:08 -0400 Subject: [PATCH 09/50] 2.0.0 --- examples/nextjs/package.json | 2 +- package-lock.json | 18 ++++++++++++++---- package.json | 2 +- packages/react/package.json | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 69e36ff..197f7d6 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "dev-console-key-api", - "version": "1.5.0", + "version": "2.0.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package-lock.json b/package-lock.json index e0c2b9a..5217541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zuplo/api-key-manager", - "version": "1.5.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zuplo/api-key-manager", - "version": "1.5.0", + "version": "2.0.0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -73,7 +73,7 @@ }, "examples/nextjs": { "name": "dev-console-key-api", - "version": "1.5.0", + "version": "2.0.0", "dependencies": { "@auth0/auth0-react": "2.2.0", "@headlessui/react": "^1.7.16", @@ -101,6 +101,16 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "examples/nextjs/node_modules/@zuplo/react-api-key-manager": { + "version": "1.5.0", + "resolved": "https://npm.pkg.github.com/download/@zuplo/react-api-key-manager/1.5.0/d0cc0de0d9bb9fa602e78b44d19c45c1d8a7d18f", + "integrity": "sha512-zWIVr5todwYdckV+NcsTKyl3sw55BQTp9+RwEJYte6qUEkjzL6iDwB085QPriF1597pMeZlFwFb9dGs/3KtJKA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || 18.x", + "react-dom": "^16.8.0 || 18.x" + } + }, "examples/nextjs/node_modules/eslint": { "version": "8.44.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", @@ -6283,7 +6293,7 @@ }, "packages/react": { "name": "@zuplo/react-api-key-manager", - "version": "1.5.0", + "version": "2.0.0", "license": "MIT", "devDependencies": { "@types/node": "^20.3.1", diff --git a/package.json b/package.json index 06a6df0..6d6c3d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/api-key-manager", - "version": "1.5.0", + "version": "2.0.0", "description": "A React component to manage API keys", "license": "MIT", "author": "Zuplo", diff --git a/packages/react/package.json b/packages/react/package.json index 909d3f3..7b7ed6b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/react-api-key-manager", - "version": "1.5.0", + "version": "2.0.0", "description": "A React component to manage API keys", "keywords": [ "react", From a35e592b559e15860282cafb68b9c1fb022f6dab Mon Sep 17 00:00:00 2001 From: Nathan Totten Date: Thu, 10 Aug 2023 17:46:11 -0400 Subject: [PATCH 10/50] updated package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d6c3d2..11cffb8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Zuplo", "repository": { "type": "git", - "url": "https://github.com/zuplo/react-api-key-manager" + "url": "https://github.com/zuplo/api-key-manager" }, "scripts": { "build": "npm run build --workspace packages/react", From 7893bc7e30a6a869f3daaf1172e14525042f1036 Mon Sep 17 00:00:00 2001 From: Nathan Totten Date: Thu, 10 Aug 2023 17:52:46 -0400 Subject: [PATCH 11/50] fix package.json --- examples/nextjs/package.json | 2 +- package-lock.json | 32 +------------------------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 197f7d6..d47d036 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -15,7 +15,7 @@ "@types/node": "20.4.2", "@types/react": "18.2.14", "@types/react-dom": "18.2.7", - "@zuplo/react-api-key-manager": "<=1", + "@zuplo/react-api-key-manager": "^2", "autoprefixer": "10.4.14", "eslint": "8.44.0", "eslint-config-next": "13.4.9", diff --git a/package-lock.json b/package-lock.json index 5217541..b82f8bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,26 +51,6 @@ "typescript": "5.1.6" } }, - "demo": { - "name": "api-key-live-sample", - "version": "0.1.0", - "extraneous": true, - "dependencies": { - "@types/node": "20.4.5", - "@types/react": "18.2.17", - "@types/react-dom": "18.2.7", - "@zuplo/react-api-key-manager": "*", - "autoprefixer": "10.4.14", - "eslint": "8.46.0", - "eslint-config-next": "13.4.12", - "next": "13.4.12", - "postcss": "8.4.27", - "react": "18.2.0", - "react-dom": "18.2.0", - "tailwindcss": "3.3.3", - "typescript": "5.1.6" - } - }, "examples/nextjs": { "name": "dev-console-key-api", "version": "2.0.0", @@ -81,7 +61,7 @@ "@types/node": "20.4.2", "@types/react": "18.2.14", "@types/react-dom": "18.2.7", - "@zuplo/react-api-key-manager": "<=1", + "@zuplo/react-api-key-manager": "^2", "autoprefixer": "10.4.14", "eslint": "8.44.0", "eslint-config-next": "13.4.9", @@ -101,16 +81,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "examples/nextjs/node_modules/@zuplo/react-api-key-manager": { - "version": "1.5.0", - "resolved": "https://npm.pkg.github.com/download/@zuplo/react-api-key-manager/1.5.0/d0cc0de0d9bb9fa602e78b44d19c45c1d8a7d18f", - "integrity": "sha512-zWIVr5todwYdckV+NcsTKyl3sw55BQTp9+RwEJYte6qUEkjzL6iDwB085QPriF1597pMeZlFwFb9dGs/3KtJKA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || 18.x", - "react-dom": "^16.8.0 || 18.x" - } - }, "examples/nextjs/node_modules/eslint": { "version": "8.44.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", From b2e963b424656effeba97db13c1701bffbce1dcd Mon Sep 17 00:00:00 2001 From: Josh Twist Date: Thu, 10 Aug 2023 15:16:35 -0700 Subject: [PATCH 12/50] added new refresh-provider (#26) * added new refresh-provider * added missing files --- examples/nextjs/.env.local | 2 +- examples/nextjs/src/components/KeyManager.tsx | 19 +++++++++-------- .../react/src/components/ApiKeyManager.tsx | 18 ++++++++++++++++ packages/react/src/default-provider.ts | 21 +------------------ packages/react/src/index.tsx | 1 + packages/react/src/interfaces.ts | 2 -- packages/react/src/refresh-provider.ts | 14 +++++++++++++ 7 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 packages/react/src/refresh-provider.ts diff --git a/examples/nextjs/.env.local b/examples/nextjs/.env.local index 1715d52..4f55375 100644 --- a/examples/nextjs/.env.local +++ b/examples/nextjs/.env.local @@ -1,4 +1,4 @@ -NEXT_PUBLIC_API_URL= +NEXT_PUBLIC_API_URL=https://sample-auth-translation-api-main-d1b33d3.d2.zuplo.dev NEXT_PUBLIC_AUTH0_DOMAIN=zuplo-samples.us.auth0.com NEXT_PUBLIC_AUTH0_CLIENT_ID=OFNbP5hhtsCHkBsXHEtWO72kKQvJtgI3 NEXT_PUBLIC_AUTH0_AUDIENCE=https://api.example.com/ diff --git a/examples/nextjs/src/components/KeyManager.tsx b/examples/nextjs/src/components/KeyManager.tsx index 1a71d51..cfa082a 100644 --- a/examples/nextjs/src/components/KeyManager.tsx +++ b/examples/nextjs/src/components/KeyManager.tsx @@ -1,5 +1,6 @@ import ApiKeyManager, { DefaultApiKeyManagerProvider, + RefreshProvider, } from "@zuplo/react-api-key-manager"; import { useContext, useMemo } from "react"; import { ThemeContext } from "@/contexts/ThemeContext"; @@ -12,16 +13,16 @@ interface Props { export default function KeyManager({ apiUrl, accessToken }: Props) { const [theme] = useContext(ThemeContext); - const provider = useMemo(() => { - return new DefaultApiKeyManagerProvider(apiUrl, accessToken); - }, [apiUrl, accessToken]); + const provider = new DefaultApiKeyManagerProvider(apiUrl, accessToken); return ( - + <> + + ); } diff --git a/packages/react/src/components/ApiKeyManager.tsx b/packages/react/src/components/ApiKeyManager.tsx index 5ddde52..d08a7dd 100644 --- a/packages/react/src/components/ApiKeyManager.tsx +++ b/packages/react/src/components/ApiKeyManager.tsx @@ -6,6 +6,7 @@ import styles from "./ApiKeyManager.module.css"; import { useEffect, useState } from "react"; import { DataContext, ProviderContext } from "./context"; import CreateConsumer from "./CreateConsumer"; +import { RefreshProvider, refreshEventName } from "../refresh-provider"; type ThemeOptions = "light" | "dark" | "system"; type Theme = "light" | "dark"; @@ -18,6 +19,7 @@ interface Props { provider: ApiKeyManagerProvider; enableCreateConsumer?: boolean; enableDeleteConsumer?: boolean; + refreshProvider?: RefreshProvider; } const getSystemDefaultThemePreference = (): Theme => { @@ -48,6 +50,7 @@ function ApiKeyManager({ theme = "light", enableCreateConsumer, enableDeleteConsumer, + refreshProvider, }: Props) { const themeStyle = `zp-key-manager--${getTheme(theme)}`; const [dataModel, setDataModel] = useState(DEFAULT_DATA_MODEL); @@ -64,6 +67,21 @@ function ApiKeyManager({ } }; + useEffect(() => { + if (refreshProvider) { + const refreshCallback = () => { + void loadData(provider); + }; + + refreshProvider.addEventListener(refreshEventName, refreshCallback); + + return () => { + refreshProvider.removeEventListener(refreshEventName, refreshCallback); + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [refreshProvider, provider]); + useEffect(() => { void loadData(provider); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/react/src/default-provider.ts b/packages/react/src/default-provider.ts index 0fccf58..ba44db4 100644 --- a/packages/react/src/default-provider.ts +++ b/packages/react/src/default-provider.ts @@ -1,4 +1,4 @@ -import { ApiKeyManagerProvider, RegisterHandle } from "./interfaces"; +import { ApiKeyManagerProvider } from "./interfaces"; export class DefaultApiKeyManagerProvider implements ApiKeyManagerProvider { constructor(baseUrl: string, token: string) { @@ -105,25 +105,6 @@ export class DefaultApiKeyManagerProvider implements ApiKeyManagerProvider { await this.innerFetch(`/consumers/${consumerName}`, 204, "DELETE"); }; - readonly #callbacks: Map void> = new Map(); - - registerOnRefresh = async (callback: () => void) => { - const handle = new RegisterHandle(); - this.#callbacks.set(handle, callback); - console.log( - `registerOnRefresh callback, now have ${this.#callbacks.size} callbacks`, - ); - }; - - unregisterOnRefresh = async (handle: RegisterHandle) => { - this.#callbacks.delete(handle); - `unregisterOnRefresh callback, now have ${this.#callbacks.size} callbacks`; - }; - - refresh = () => { - this.#callbacks.forEach((callback) => callback()); - }; - updateConsumerDescription = async ( consumerName: string, description: string, diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 098d830..c5820aa 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -2,6 +2,7 @@ import ApiKeyManager from "./components/ApiKeyManager"; export default ApiKeyManager; export { DefaultApiKeyManagerProvider } from "./default-provider"; +export { RefreshProvider } from "./refresh-provider"; export type { ApiKey, ApiKeyManagerProvider, diff --git a/packages/react/src/interfaces.ts b/packages/react/src/interfaces.ts index 56af01f..7daa0ad 100644 --- a/packages/react/src/interfaces.ts +++ b/packages/react/src/interfaces.ts @@ -1,5 +1,3 @@ -export class RegisterHandle {} - export interface ApiKeyManagerProvider { getConsumers: () => Promise; rollKey: (consumerName: string, expiresOn: Date) => Promise; diff --git a/packages/react/src/refresh-provider.ts b/packages/react/src/refresh-provider.ts new file mode 100644 index 0000000..0842778 --- /dev/null +++ b/packages/react/src/refresh-provider.ts @@ -0,0 +1,14 @@ +// This component is designed to allow consumers to programmatically refresh the data model. + +export const refreshEventName = "refresh"; + +export class RefreshProvider extends EventTarget { + constructor() { + super(); + } + + refresh() { + const event = new CustomEvent(refreshEventName); + this.dispatchEvent(event); + } +} From 689bee93dc26684ecd49fcd03950d530067e7734 Mon Sep 17 00:00:00 2001 From: Josh Twist Date: Thu, 10 Aug 2023 15:16:53 -0700 Subject: [PATCH 13/50] 2.1.0 --- examples/nextjs/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- packages/react/package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index d47d036..cc48d24 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "dev-console-key-api", - "version": "2.0.0", + "version": "2.1.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package-lock.json b/package-lock.json index b82f8bd..d29691a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zuplo/api-key-manager", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zuplo/api-key-manager", - "version": "2.0.0", + "version": "2.1.0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -53,7 +53,7 @@ }, "examples/nextjs": { "name": "dev-console-key-api", - "version": "2.0.0", + "version": "2.1.0", "dependencies": { "@auth0/auth0-react": "2.2.0", "@headlessui/react": "^1.7.16", @@ -6263,7 +6263,7 @@ }, "packages/react": { "name": "@zuplo/react-api-key-manager", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "devDependencies": { "@types/node": "^20.3.1", diff --git a/package.json b/package.json index 11cffb8..c494ca5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/api-key-manager", - "version": "2.0.0", + "version": "2.1.0", "description": "A React component to manage API keys", "license": "MIT", "author": "Zuplo", diff --git a/packages/react/package.json b/packages/react/package.json index 7b7ed6b..b5d161d 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/react-api-key-manager", - "version": "2.0.0", + "version": "2.1.0", "description": "A React component to manage API keys", "keywords": [ "react", From 812e2a871f8b32e97aeeb8fb057b04dea0e2e48e Mon Sep 17 00:00:00 2001 From: Adrian Machado Date: Thu, 10 Aug 2023 15:44:04 -0700 Subject: [PATCH 14/50] Roll keys fix #27 --- packages/react/src/components/ConsumerControl.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/react/src/components/ConsumerControl.tsx b/packages/react/src/components/ConsumerControl.tsx index 7d148a6..cdb2c0f 100644 --- a/packages/react/src/components/ConsumerControl.tsx +++ b/packages/react/src/components/ConsumerControl.tsx @@ -111,11 +111,11 @@ const ConsumerControl = ({ icon: PencilSquareIcon({}), }; + // You can't roll keys if there are no keys to roll + const numRollableKeys = consumer.apiKeys.filter((k) => !k.expiresOn).length; + const enableRollKeys = numRollableKeys > 0; const rollKeysMenuItem: MenuItem = { - label: - consumer.apiKeys.filter((k) => !k.expiresOn).length > 1 - ? "Roll keys" - : "Roll key", + label: numRollableKeys > 1 ? "Roll keys" : "Roll key", action: handleRollKey, icon: ArrowPathIcon({}), }; @@ -126,8 +126,10 @@ const ConsumerControl = ({ icon: TrashIcon({}), }; - const initialMenuItems = [editLabelMenuItem, rollKeysMenuItem]; - + const initialMenuItems: MenuItem[] = [editLabelMenuItem]; + if (enableRollKeys) { + initialMenuItems.push(rollKeysMenuItem); + } if (enableDeleteConsumer) { initialMenuItems.push(deleteConsumerMenuItem); } From fee0ddf300f6c3e5a8fd1aa03e4014465a96ec8c Mon Sep 17 00:00:00 2001 From: Adrian Machado Date: Thu, 10 Aug 2023 16:01:50 -0700 Subject: [PATCH 15/50] Add consumer to action #28 --- packages/react/src/components/ConsumerControl.tsx | 2 +- packages/react/src/components/SimpleMenu.tsx | 9 +++++---- packages/react/src/interfaces.ts | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/react/src/components/ConsumerControl.tsx b/packages/react/src/components/ConsumerControl.tsx index cdb2c0f..4a14ecd 100644 --- a/packages/react/src/components/ConsumerControl.tsx +++ b/packages/react/src/components/ConsumerControl.tsx @@ -190,7 +190,7 @@ const ConsumerControl = ({
) : ( - +
(null); - function click(action: () => void) { + function click(action: (consumer: Consumer) => void) { setIsOpen(false); - action(); + action(consumer); } useEffect(() => { diff --git a/packages/react/src/interfaces.ts b/packages/react/src/interfaces.ts index 7daa0ad..dc0fd47 100644 --- a/packages/react/src/interfaces.ts +++ b/packages/react/src/interfaces.ts @@ -12,7 +12,7 @@ export interface ApiKeyManagerProvider { export interface MenuItem { label: string; - action: () => void; + action: (consumer: Consumer) => void; icon?: JSX.Element; } From 918cdf9dad16c0e700cec192c30b41bcff70e14b Mon Sep 17 00:00:00 2001 From: Integration Service Date: Thu, 10 Aug 2023 23:05:12 +0000 Subject: [PATCH 16/50] 2.2.0 --- examples/nextjs/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- packages/react/package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index cc48d24..e2c6b8c 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "dev-console-key-api", - "version": "2.1.0", + "version": "2.2.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package-lock.json b/package-lock.json index d29691a..8e8f942 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zuplo/api-key-manager", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zuplo/api-key-manager", - "version": "2.1.0", + "version": "2.2.0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -53,7 +53,7 @@ }, "examples/nextjs": { "name": "dev-console-key-api", - "version": "2.1.0", + "version": "2.2.0", "dependencies": { "@auth0/auth0-react": "2.2.0", "@headlessui/react": "^1.7.16", @@ -6263,7 +6263,7 @@ }, "packages/react": { "name": "@zuplo/react-api-key-manager", - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "devDependencies": { "@types/node": "^20.3.1", diff --git a/package.json b/package.json index c494ca5..c6d198b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/api-key-manager", - "version": "2.1.0", + "version": "2.2.0", "description": "A React component to manage API keys", "license": "MIT", "author": "Zuplo", diff --git a/packages/react/package.json b/packages/react/package.json index b5d161d..c1b16e4 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@zuplo/react-api-key-manager", - "version": "2.1.0", + "version": "2.2.0", "description": "A React component to manage API keys", "keywords": [ "react", From 0b4cdd59f9bbdb4bf41f4bb38c2272b7a4930048 Mon Sep 17 00:00:00 2001 From: Adrian Machado Date: Thu, 10 Aug 2023 16:27:29 -0700 Subject: [PATCH 17/50] Improve testability, a11y, and fix loading bug #29 * Improve testability, a11y, and fix loading bug * Fix console logs --- examples/nextjs/src/pages/keys.tsx | 2 +- packages/react/src/components/ApiKeyManager.tsx | 1 + packages/react/src/components/ConsumerControl.tsx | 9 ++++++++- packages/react/src/components/KeyControl.tsx | 4 ++++ packages/react/src/components/SimpleMenu.tsx | 6 ++++++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/nextjs/src/pages/keys.tsx b/examples/nextjs/src/pages/keys.tsx index 3fc881d..f548710 100644 --- a/examples/nextjs/src/pages/keys.tsx +++ b/examples/nextjs/src/pages/keys.tsx @@ -29,7 +29,7 @@ function Keys() { // If the user is not authenticated, redirect to the index page if (!isLoading && !isAuthenticated) { - console.log("Keys not authenticated"); + console.warn("Keys not authenticated"); router.push("/"); } diff --git a/packages/react/src/components/ApiKeyManager.tsx b/packages/react/src/components/ApiKeyManager.tsx index d08a7dd..9b92915 100644 --- a/packages/react/src/components/ApiKeyManager.tsx +++ b/packages/react/src/components/ApiKeyManager.tsx @@ -63,6 +63,7 @@ function ApiKeyManager({ setDataModel({ consumers: result.data, isFetching: false }); } catch (err) { setError((err as Error).message); + setDataModel({ consumers: undefined, isFetching: false }); console.error(err); } }; diff --git a/packages/react/src/components/ConsumerControl.tsx b/packages/react/src/components/ConsumerControl.tsx index 4a14ecd..0e7125d 100644 --- a/packages/react/src/components/ConsumerControl.tsx +++ b/packages/react/src/components/ConsumerControl.tsx @@ -143,6 +143,7 @@ const ConsumerControl = ({ {edit ? (
event.target.select()} onKeyUp={(event) => { @@ -155,6 +156,7 @@ const ConsumerControl = ({ defaultValue={consumer.description} />
) : ( -
+
{consumer.description ?? consumer.name}
)} @@ -213,6 +219,7 @@ const ConsumerControl = ({
) : (
) : (
{consumer.description ?? consumer.name} @@ -219,7 +219,7 @@ const ConsumerControl = ({
) : (