From fd80db962d4c070790911dcec7fb6a07fdfca6e9 Mon Sep 17 00:00:00 2001 From: Cafer Elgin Date: Wed, 27 Nov 2024 00:58:02 +0200 Subject: [PATCH 1/5] feat: added instructions modal to pagerduty modal --- .../src/components/InstructionsModal.tsx | 104 ++++++++++++++++++ .../src/components/PagerDutyPlugin.tsx | 28 ++++- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 plugins/pagerduty-incidents/src/components/InstructionsModal.tsx diff --git a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx new file mode 100644 index 0000000..66134f4 --- /dev/null +++ b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx @@ -0,0 +1,104 @@ +import type React from "react"; + +import { + Text, + Box, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + Button, + FormControl, + FormLabel, + FormHelperText, + Input, + FormErrorMessage, + Flex, +} from "@chakra-ui/react"; +import { useState } from "react"; + +interface InstructionsProps { + isOpen: boolean; + onClose: () => void; +} + +const InstructionsModal: React.FC = ({ + isOpen, + onClose, +}) => { + const [tokenInput, setTokenInput] = useState(""); + const [isError, setIsError] = useState(false); + const [isSubmitDisabled, setIsSubmitDisabled] = useState(true); + + const handleInputChange = (e: React.ChangeEvent): void => { + setTokenInput(e.target.value); + setIsSubmitDisabled(false); // todo: validate token before enabling submit + setIsError(false); // todo: if token is invalid, set isError to true + }; + + const handleFormSubmit = (e: React.FormEvent): void => { + // todo: handle form submit here + e.preventDefault(); + onClose(); + }; + + return ( + + + + Instructions + + + + This plugin makes it possible to view PagerDuty incidents. + + + To get started, please add your PagerDuty REST API token: + + + +
+ + PagerDuty REST API Token + + {!isError ? ( + + You can generate REST API Token from your PagerDuty + dashboard. + + ) : ( + + Please enter a valid token. + + )} + + + + +
+
+
+
+
+
+ ); +}; + +export default InstructionsModal; diff --git a/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx b/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx index ed0b480..667928a 100644 --- a/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx +++ b/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx @@ -10,10 +10,11 @@ import { usePluginContext, } from "@cortexapps/plugin-core/components"; -import { Text } from "@chakra-ui/react"; +import { Text, useDisclosure } from "@chakra-ui/react"; import PagerDutyIncidents from "./PagerDutyIncidents"; import PagerDutyPicker from "./PagerDutyPicker"; +import InstructionsModal from "./InstructionsModal"; const PagerDutyPlugin: React.FC = () => { const context = usePluginContext(); @@ -21,6 +22,27 @@ const PagerDutyPlugin: React.FC = () => { Record | undefined >(); + const { + isOpen: isInstuctionsModalOpen, + onOpen: onInstructionsModalOpen, + onClose: onInstructionsModalClose, + } = useDisclosure(); + + useEffect(() => { + // checks if instructions modal is needed + const checkPagerDutyToken = async (): Promise => { + try { + const response = await fetch(`https://api.pagerduty.com/abilities`); + + if (response.status === 401) { + onInstructionsModalOpen(); + } + // const data = await response.json(); + } catch (e) {} + }; + void checkPagerDutyToken(); + }, []); + const [hasGitops, setHasGitops] = useState(null); useEffect(() => { if (!context?.entity?.tag || !context?.apiBaseUrl) { @@ -104,6 +126,10 @@ const PagerDutyPlugin: React.FC = () => { )} + ); }; From c8ef5720197aa70ea3956f141e3cf9aae464ab64 Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Tue, 26 Nov 2024 23:01:52 -0500 Subject: [PATCH 2/5] check token, move modal --- .../src/components/Instructions.tsx | 32 +++++++ .../src/components/InstructionsModal.tsx | 43 ++++----- .../src/components/PagerDutyIncidents.tsx | 2 +- .../src/components/PagerDutyPicker.tsx | 5 +- .../src/components/PagerDutyPlugin.tsx | 40 +++----- .../{hooks.tsx => hooks/pagerDutyHooks.tsx} | 93 +++++++++++-------- .../pagerduty-incidents/src/hooks/uiHooks.tsx | 43 +++++++++ 7 files changed, 169 insertions(+), 89 deletions(-) create mode 100644 plugins/pagerduty-incidents/src/components/Instructions.tsx rename plugins/pagerduty-incidents/src/{hooks.tsx => hooks/pagerDutyHooks.tsx} (83%) create mode 100644 plugins/pagerduty-incidents/src/hooks/uiHooks.tsx diff --git a/plugins/pagerduty-incidents/src/components/Instructions.tsx b/plugins/pagerduty-incidents/src/components/Instructions.tsx new file mode 100644 index 0000000..47098e3 --- /dev/null +++ b/plugins/pagerduty-incidents/src/components/Instructions.tsx @@ -0,0 +1,32 @@ +import type React from "react"; + +import { Text, Box, Button, useDisclosure } from "@chakra-ui/react"; + +import InstructionsModal from "./InstructionsModal"; + +const Instructions: React.FC = () => { + const { + isOpen: isInstuctionsModalOpen, + onOpen: onInstructionsModalOpen, + onClose: onInstructionsModalClose, + } = useDisclosure(); + + return ( + + + This plugin makes it possible to view PagerDuty incidents associated with an + entity. To get started, you need a PagerDuty API key and permissions to create + proxies and secrets in Cortex. Click the button below to configure the plugin. + + + + + ); +} + +export default Instructions; diff --git a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx index 66134f4..9720a2f 100644 --- a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx +++ b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx @@ -1,7 +1,7 @@ import type React from "react"; +import { useState, useEffect } from "react"; import { - Text, Box, Modal, ModalOverlay, @@ -11,12 +11,12 @@ import { Button, FormControl, FormLabel, - FormHelperText, Input, FormErrorMessage, Flex, } from "@chakra-ui/react"; -import { useState } from "react"; + +import { isPagerDutyTokenValid } from "../hooks/pagerDutyHooks"; interface InstructionsProps { isOpen: boolean; @@ -28,15 +28,24 @@ const InstructionsModal: React.FC = ({ onClose, }) => { const [tokenInput, setTokenInput] = useState(""); - const [isError, setIsError] = useState(false); - const [isSubmitDisabled, setIsSubmitDisabled] = useState(true); + const [debouncedTokenInput, setDebouncedTokenInput] = useState(""); + + const tokenIsValid = isPagerDutyTokenValid(debouncedTokenInput); const handleInputChange = (e: React.ChangeEvent): void => { setTokenInput(e.target.value); - setIsSubmitDisabled(false); // todo: validate token before enabling submit - setIsError(false); // todo: if token is invalid, set isError to true }; + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedTokenInput(tokenInput); + }, 100); + + return () => { + clearTimeout(handler); + }; + }, [tokenInput]); + const handleFormSubmit = (e: React.FormEvent): void => { // todo: handle form submit here e.preventDefault(); @@ -52,19 +61,12 @@ const InstructionsModal: React.FC = ({ > - Instructions + Configure Plugin Proxy - - This plugin makes it possible to view PagerDuty incidents. - - - To get started, please add your PagerDuty REST API token: - -
- + PagerDuty REST API Token = ({ value={tokenInput} onChange={handleInputChange} /> - {!isError ? ( - - You can generate REST API Token from your PagerDuty - dashboard. - - ) : ( + {!tokenIsValid && ( Please enter a valid token. @@ -87,7 +84,7 @@ const InstructionsModal: React.FC = ({ diff --git a/plugins/pagerduty-incidents/src/components/PagerDutyIncidents.tsx b/plugins/pagerduty-incidents/src/components/PagerDutyIncidents.tsx index 6f86be6..dcb4a41 100644 --- a/plugins/pagerduty-incidents/src/components/PagerDutyIncidents.tsx +++ b/plugins/pagerduty-incidents/src/components/PagerDutyIncidents.tsx @@ -8,7 +8,7 @@ import { usePagerDutyService, usePagerDutyIncidents, usePagerDutyOnCalls, -} from "../hooks"; +} from "../hooks/pagerDutyHooks"; import OnCallBadges from "./OnCallBadges"; import "../baseStyles.css"; diff --git a/plugins/pagerduty-incidents/src/components/PagerDutyPicker.tsx b/plugins/pagerduty-incidents/src/components/PagerDutyPicker.tsx index be8ef45..ec5322b 100644 --- a/plugins/pagerduty-incidents/src/components/PagerDutyPicker.tsx +++ b/plugins/pagerduty-incidents/src/components/PagerDutyPicker.tsx @@ -8,9 +8,12 @@ import { parseDocument } from "yaml"; import { usePagerDutyServices, +} from "../hooks/pagerDutyHooks"; + +import { useErrorToast, useErrorToastForResponse, -} from "../hooks"; +} from "../hooks/uiHooks"; interface PagerDutyPickerProps { entityYaml: Record; diff --git a/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx b/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx index 667928a..5067f26 100644 --- a/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx +++ b/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx @@ -7,14 +7,17 @@ import { Stack, Title, Link, + Loader, usePluginContext, } from "@cortexapps/plugin-core/components"; -import { Text, useDisclosure } from "@chakra-ui/react"; +import { Text } from "@chakra-ui/react"; + +import { isPagerDutyConfigured } from "../hooks/pagerDutyHooks"; import PagerDutyIncidents from "./PagerDutyIncidents"; import PagerDutyPicker from "./PagerDutyPicker"; -import InstructionsModal from "./InstructionsModal"; +import Instructions from "./Instructions"; const PagerDutyPlugin: React.FC = () => { const context = usePluginContext(); @@ -22,26 +25,7 @@ const PagerDutyPlugin: React.FC = () => { Record | undefined >(); - const { - isOpen: isInstuctionsModalOpen, - onOpen: onInstructionsModalOpen, - onClose: onInstructionsModalClose, - } = useDisclosure(); - - useEffect(() => { - // checks if instructions modal is needed - const checkPagerDutyToken = async (): Promise => { - try { - const response = await fetch(`https://api.pagerduty.com/abilities`); - - if (response.status === 401) { - onInstructionsModalOpen(); - } - // const data = await response.json(); - } catch (e) {} - }; - void checkPagerDutyToken(); - }, []); + const isConfigured = isPagerDutyConfigured(); const [hasGitops, setHasGitops] = useState(null); useEffect(() => { @@ -84,6 +68,14 @@ const PagerDutyPlugin: React.FC = () => { void fetchEntityYaml(); }, [fetchEntityYaml, rerender]); + if (isConfigured === null) { + return ; + } + + if (isConfigured === false) { + return ; + } + return (
{!isEmpty(entityYaml) && ( @@ -126,10 +118,6 @@ const PagerDutyPlugin: React.FC = () => { )} -
); }; diff --git a/plugins/pagerduty-incidents/src/hooks.tsx b/plugins/pagerduty-incidents/src/hooks/pagerDutyHooks.tsx similarity index 83% rename from plugins/pagerduty-incidents/src/hooks.tsx rename to plugins/pagerduty-incidents/src/hooks/pagerDutyHooks.tsx index 856c638..a6de726 100644 --- a/plugins/pagerduty-incidents/src/hooks.tsx +++ b/plugins/pagerduty-incidents/src/hooks/pagerDutyHooks.tsx @@ -1,6 +1,5 @@ -import { useState, useEffect, useCallback } from "react"; -import { useToast } from "@chakra-ui/react"; -import { cortexResponseError } from "./util"; +import { useState, useEffect } from "react"; +import { useErrorToastForResponse } from "./uiHooks"; export interface UsePagerDutyServicesReturn { services: Array>; @@ -26,44 +25,62 @@ export interface UsePagerDutyOnCallsReturn { errorMessage: string; } -interface ErrorToastProps { - title?: string; - message?: string; +export const isPagerDutyTokenValid = (token: string): boolean => { + const [isValid, setIsValid] = useState(false); + + useEffect(() => { + if (!token) { + setIsValid(false); + return; + } + + const checkPagerDutyAbilities = async (): Promise => { + try { + const response = await fetch(`https://api.pagerduty.com/abilities`, { + headers: { + Authorization: `Token token=${token}`, + }, + }); + + if (response.ok) { + setIsValid(true); + } else { + setIsValid(false); + } + } catch (e) { + setIsValid(false); + } + }; + + void checkPagerDutyAbilities(); + }, [token]); + + return isValid; } -export const useErrorToast = (): ((props: ErrorToastProps) => void) => { - const toast = useToast(); - const errorToast = useCallback( - ({ title = "Error", message = "An error occurred" }: ErrorToastProps) => { - toast({ - title, - description: message, - status: "error", - duration: 5000, - isClosable: true, - }); - }, - [toast] - ); - - return errorToast; -}; +export const isPagerDutyConfigured = (): boolean | null => { + const [isConfigured, setIsConfigured] = useState(null); -export const useErrorToastForResponse = (): ((response: Response) => void) => { - const errorToast = useErrorToast(); - const errorToastForResponse = useCallback( - (response: Response) => { - const { status, message } = cortexResponseError(response); - errorToast({ - title: `HTTP Error ${status}`, - message, - }); - }, - [errorToast] - ); - - return errorToastForResponse; -}; + useEffect(() => { + const checkPagerDutyAbilities = async (): Promise => { + try { + const response = await fetch(`https://api.pagerduty.com/abilities`); + + if (response.ok) { + setIsConfigured(true); + } else { + setIsConfigured(false); + } + } catch (e) { + setIsConfigured(false); + } + }; + + void checkPagerDutyAbilities(); + }, []); + + return isConfigured; +} export const usePagerDutyServices = (): UsePagerDutyServicesReturn => { const [services, setServices] = useState([]); diff --git a/plugins/pagerduty-incidents/src/hooks/uiHooks.tsx b/plugins/pagerduty-incidents/src/hooks/uiHooks.tsx new file mode 100644 index 0000000..dca7d9d --- /dev/null +++ b/plugins/pagerduty-incidents/src/hooks/uiHooks.tsx @@ -0,0 +1,43 @@ +import { useCallback } from "react"; +import { useToast } from "@chakra-ui/react"; +import { cortexResponseError } from "../util"; + +interface ErrorToastProps { + title?: string; + message?: string; + } + + export const useErrorToast = (): ((props: ErrorToastProps) => void) => { + const toast = useToast(); + const errorToast = useCallback( + ({ title = "Error", message = "An error occurred" }: ErrorToastProps) => { + toast({ + title, + description: message, + status: "error", + duration: 5000, + isClosable: true, + }); + }, + [toast] + ); + + return errorToast; + }; + + export const useErrorToastForResponse = (): ((response: Response) => void) => { + const errorToast = useErrorToast(); + const errorToastForResponse = useCallback( + (response: Response) => { + const { status, message } = cortexResponseError(response); + errorToast({ + title: `HTTP Error ${status}`, + message, + }); + }, + [errorToast] + ); + + return errorToastForResponse; + }; + \ No newline at end of file From af98a16a53947fc1fbb4e59d1a1a9933e6bede21 Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Thu, 12 Dec 2024 11:19:42 -0500 Subject: [PATCH 3/5] add auto config --- .../src/components/App.test.tsx | 4 +- .../src/components/Instructions.tsx | 119 +++++++-- .../src/components/InstructionsModal.tsx | 241 +++++++++++++++--- .../src/components/PagerDutyPicker.tsx | 9 +- .../src/components/PagerDutyPlugin.tsx | 6 +- .../src/hooks/pagerDutyHooks.tsx | 8 +- .../pagerduty-incidents/src/hooks/uiHooks.tsx | 75 +++--- 7 files changed, 357 insertions(+), 105 deletions(-) diff --git a/plugins/pagerduty-incidents/src/components/App.test.tsx b/plugins/pagerduty-incidents/src/components/App.test.tsx index bfcb46c..3e2dc73 100644 --- a/plugins/pagerduty-incidents/src/components/App.test.tsx +++ b/plugins/pagerduty-incidents/src/components/App.test.tsx @@ -47,9 +47,7 @@ describe("App", () => { const { getByText } = render(); await waitFor(() => { - const element = getByText( - /This entity is not associated with any PagerDuty service./ - ); + const element = getByText(/Configure PagerDuty Incidents Plugin/); expect(element).toBeInTheDocument(); expect(fetch).toHaveBeenCalledWith( expect.stringMatching( diff --git a/plugins/pagerduty-incidents/src/components/Instructions.tsx b/plugins/pagerduty-incidents/src/components/Instructions.tsx index 47098e3..335f38c 100644 --- a/plugins/pagerduty-incidents/src/components/Instructions.tsx +++ b/plugins/pagerduty-incidents/src/components/Instructions.tsx @@ -1,32 +1,121 @@ import type React from "react"; -import { Text, Box, Button, useDisclosure } from "@chakra-ui/react"; +import { + Text, + Box, + Button, + Link, + useDisclosure, + Heading, +} from "@chakra-ui/react"; import InstructionsModal from "./InstructionsModal"; +import { useState, useEffect } from "react"; + +import { usePluginContext } from "@cortexapps/plugin-core/components"; const Instructions: React.FC = () => { + const { apiBaseUrl } = usePluginContext(); const { isOpen: isInstuctionsModalOpen, onOpen: onInstructionsModalOpen, onClose: onInstructionsModalClose, } = useDisclosure(); + const [isMarketplacePlugin, setIsMarketplacePlugin] = useState< + boolean | null + >(null); + const [configCompleted, setConfigCompleted] = useState(false); + + useEffect(() => { + const fetchMarketplacePlugin = async (): Promise => { + try { + const response = await fetch( + `${apiBaseUrl}/plugins/pagerduty-incidents` + ); + const { description } = await response.json(); + setIsMarketplacePlugin( + description.includes( + "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html" + ) + ); + } catch (e) { + setIsMarketplacePlugin(false); + } + }; + void fetchMarketplacePlugin(); + }, [apiBaseUrl]); + + const onConfigCompleted = (): void => { + setConfigCompleted(true); + }; + + if (configCompleted) { + return ( + + + Configure PagerDuty Incidents Plugin + + + Configuration completed successfully. Please refresh the page to start + using the plugin. + + + ); + } + return ( - - - This plugin makes it possible to view PagerDuty incidents associated with an - entity. To get started, you need a PagerDuty API key and permissions to create - proxies and secrets in Cortex. Click the button below to configure the plugin. - - - + + + Configure PagerDuty Incidents Plugin + + {isMarketplacePlugin && ( + <> + + To configure this plugin automatically, you need a PagerDuty API key + and permissions to create proxies and secrets in Cortex. Click the + button below to enter your PagerDuty API key and do automatic + configuration. + + + + + )} + {isMarketplacePlugin === false && ( + + This plugin was not installed by the Plugin Marketplace, so it cannot + be configured automatically. To configure it manually, follow the + instructions in the plugin documentation{" "} + + here + + . + + )} ); -} +}; export default Instructions; diff --git a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx index 9720a2f..d069640 100644 --- a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx +++ b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx @@ -1,5 +1,5 @@ import type React from "react"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { Box, @@ -16,21 +16,36 @@ import { Flex, } from "@chakra-ui/react"; -import { isPagerDutyTokenValid } from "../hooks/pagerDutyHooks"; +import { usePluginContext } from "@cortexapps/plugin-core/components"; -interface InstructionsProps { +import { useErrorToast } from "../hooks/uiHooks"; +import { useIsPagerDutyTokenValid } from "../hooks/pagerDutyHooks"; + +interface InstructionsModalProps { isOpen: boolean; onClose: () => void; + onConfigCompleted: () => void; } -const InstructionsModal: React.FC = ({ +const InstructionsModal: React.FC = ({ isOpen, onClose, + onConfigCompleted, }) => { + const { apiBaseUrl } = usePluginContext(); + + const apiOrigin = useMemo( + () => (apiBaseUrl ? new URL(apiBaseUrl).origin : ""), + [apiBaseUrl] + ); + + const internalBaseUrl = `${apiOrigin}/api/internal/v1`; + const [tokenInput, setTokenInput] = useState(""); const [debouncedTokenInput, setDebouncedTokenInput] = useState(""); - const tokenIsValid = isPagerDutyTokenValid(debouncedTokenInput); + const tokenIsValid = useIsPagerDutyTokenValid(debouncedTokenInput); + const errorToast = useErrorToast(); const handleInputChange = (e: React.ChangeEvent): void => { setTokenInput(e.target.value); @@ -46,11 +61,169 @@ const InstructionsModal: React.FC = ({ }; }, [tokenInput]); - const handleFormSubmit = (e: React.FormEvent): void => { - // todo: handle form submit here - e.preventDefault(); - onClose(); - }; + const doAddSecret = useCallback(async (): Promise => { + console.log("Adding secret"); + const body = JSON.stringify({ + name: "pagerduty_secret", + tag: "pagerduty_secret", + secret: debouncedTokenInput, + }); + + try { + const response = await fetch(`${internalBaseUrl}/secrets`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }); + + if (!response?.ok) { + const message = + (response as any).cortexResponse?.statusText || + response.statusText || + "An error occurred"; + throw new Error(message); + } + return true; + } catch (e) { + console.log("Failed to add secret", e); + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to add secret", + message, + }); + } + return false; + }, [internalBaseUrl, debouncedTokenInput, errorToast]); + + const doAddProxy = useCallback(async (): Promise => { + const body = JSON.stringify({ + name: "PagerDuty Plugin Proxy", + tag: "pagerduty-plugin-proxy", + urlConfigurations: { + "https://api.pagerduty.com": { + urlHeaders: [ + { + name: "Authorization", + value: `Token token={{{secrets.pagerduty_secret}}}`, + }, + ], + }, + }, + }); + + try { + const response = await fetch(`${internalBaseUrl}/proxies`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }); + + if (!response?.ok) { + const message = + (response as any).cortexResponse?.statusText || + response.statusText || + "An error occurred"; + throw new Error(message); + } + return true; + } catch (e) { + console.error("Failed to add proxy", e); + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to add proxy", + message, + }); + } + return false; + }, [internalBaseUrl, errorToast]); + + const doUpdatePlugin = useCallback(async (): Promise => { + try { + let response = await fetch(`${apiBaseUrl}/plugins/pagerduty-incidents`); + if (!response.ok) { + throw new Error("Failed to fetch plugin metadata"); + } + const plugin = await response.json(); + const pluginDescription = plugin.description || ""; + if ( + !pluginDescription.includes( + "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html" + ) + ) { + throw new Error( + "This pagerduty-incidents plugin was not installed by the Plugin Marketplace" + ); + } + response = await fetch( + "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html" + ); + if (!response.ok) { + throw new Error("Failed to fetch plugin UI"); + } + const ui = await response.text(); + plugin.proxyTag = "pagerduty-plugin-proxy"; + plugin.blob = ui; + response = await fetch(`${apiBaseUrl}/plugins/pagerduty-incidents`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(plugin), + }); + if (!response.ok) { + throw new Error("Failed to update plugin"); + } + return true; + } catch (e) { + console.error("Failed to update plugin", e); + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to update plugin", + message, + }); + } + return false; + }, [apiBaseUrl, errorToast]); + + const handleFormSubmit = useCallback(() => { + const handleAsync = async ( + e: React.MouseEvent + ): Promise => { + e.preventDefault(); + const didUpdatePlugin = + (await doAddSecret()) && + (await doAddProxy()) && + (await doUpdatePlugin()); + setTokenInput(""); + if (didUpdatePlugin) { + onConfigCompleted(); + } + onClose(); + }; + void handleAsync; + }, [onClose, doAddSecret, doAddProxy, doUpdatePlugin, onConfigCompleted]); return ( = ({ - - - PagerDuty REST API Token - - {!tokenIsValid && ( - - Please enter a valid token. - - )} - - - - - + + PagerDuty REST API Token + + {!tokenIsValid && ( + + Please enter a valid token. + + )} + + + + diff --git a/plugins/pagerduty-incidents/src/components/PagerDutyPicker.tsx b/plugins/pagerduty-incidents/src/components/PagerDutyPicker.tsx index ec5322b..120626e 100644 --- a/plugins/pagerduty-incidents/src/components/PagerDutyPicker.tsx +++ b/plugins/pagerduty-incidents/src/components/PagerDutyPicker.tsx @@ -6,14 +6,9 @@ import { usePluginContext } from "@cortexapps/plugin-core/components"; import { Flex, Button, Select } from "@chakra-ui/react"; import { parseDocument } from "yaml"; -import { - usePagerDutyServices, -} from "../hooks/pagerDutyHooks"; +import { usePagerDutyServices } from "../hooks/pagerDutyHooks"; -import { - useErrorToast, - useErrorToastForResponse, -} from "../hooks/uiHooks"; +import { useErrorToast, useErrorToastForResponse } from "../hooks/uiHooks"; interface PagerDutyPickerProps { entityYaml: Record; diff --git a/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx b/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx index 5067f26..8212188 100644 --- a/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx +++ b/plugins/pagerduty-incidents/src/components/PagerDutyPlugin.tsx @@ -13,7 +13,7 @@ import { import { Text } from "@chakra-ui/react"; -import { isPagerDutyConfigured } from "../hooks/pagerDutyHooks"; +import { useIsPagerDutyConfigured } from "../hooks/pagerDutyHooks"; import PagerDutyIncidents from "./PagerDutyIncidents"; import PagerDutyPicker from "./PagerDutyPicker"; @@ -25,7 +25,7 @@ const PagerDutyPlugin: React.FC = () => { Record | undefined >(); - const isConfigured = isPagerDutyConfigured(); + const isConfigured = useIsPagerDutyConfigured(); const [hasGitops, setHasGitops] = useState(null); useEffect(() => { @@ -72,7 +72,7 @@ const PagerDutyPlugin: React.FC = () => { return ; } - if (isConfigured === false) { + if (!isConfigured) { return ; } diff --git a/plugins/pagerduty-incidents/src/hooks/pagerDutyHooks.tsx b/plugins/pagerduty-incidents/src/hooks/pagerDutyHooks.tsx index a6de726..4049721 100644 --- a/plugins/pagerduty-incidents/src/hooks/pagerDutyHooks.tsx +++ b/plugins/pagerduty-incidents/src/hooks/pagerDutyHooks.tsx @@ -25,7 +25,7 @@ export interface UsePagerDutyOnCallsReturn { errorMessage: string; } -export const isPagerDutyTokenValid = (token: string): boolean => { +export const useIsPagerDutyTokenValid = (token: string): boolean => { const [isValid, setIsValid] = useState(false); useEffect(() => { @@ -56,9 +56,9 @@ export const isPagerDutyTokenValid = (token: string): boolean => { }, [token]); return isValid; -} +}; -export const isPagerDutyConfigured = (): boolean | null => { +export const useIsPagerDutyConfigured = (): boolean | null => { const [isConfigured, setIsConfigured] = useState(null); useEffect(() => { @@ -80,7 +80,7 @@ export const isPagerDutyConfigured = (): boolean | null => { }, []); return isConfigured; -} +}; export const usePagerDutyServices = (): UsePagerDutyServicesReturn => { const [services, setServices] = useState([]); diff --git a/plugins/pagerduty-incidents/src/hooks/uiHooks.tsx b/plugins/pagerduty-incidents/src/hooks/uiHooks.tsx index dca7d9d..6f390e1 100644 --- a/plugins/pagerduty-incidents/src/hooks/uiHooks.tsx +++ b/plugins/pagerduty-incidents/src/hooks/uiHooks.tsx @@ -3,41 +3,40 @@ import { useToast } from "@chakra-ui/react"; import { cortexResponseError } from "../util"; interface ErrorToastProps { - title?: string; - message?: string; - } - - export const useErrorToast = (): ((props: ErrorToastProps) => void) => { - const toast = useToast(); - const errorToast = useCallback( - ({ title = "Error", message = "An error occurred" }: ErrorToastProps) => { - toast({ - title, - description: message, - status: "error", - duration: 5000, - isClosable: true, - }); - }, - [toast] - ); - - return errorToast; - }; - - export const useErrorToastForResponse = (): ((response: Response) => void) => { - const errorToast = useErrorToast(); - const errorToastForResponse = useCallback( - (response: Response) => { - const { status, message } = cortexResponseError(response); - errorToast({ - title: `HTTP Error ${status}`, - message, - }); - }, - [errorToast] - ); - - return errorToastForResponse; - }; - \ No newline at end of file + title?: string; + message?: string; +} + +export const useErrorToast = (): ((props: ErrorToastProps) => void) => { + const toast = useToast(); + const errorToast = useCallback( + ({ title = "Error", message = "An error occurred" }: ErrorToastProps) => { + toast({ + title, + description: message, + status: "error", + duration: 5000, + isClosable: true, + }); + }, + [toast] + ); + + return errorToast; +}; + +export const useErrorToastForResponse = (): ((response: Response) => void) => { + const errorToast = useErrorToast(); + const errorToastForResponse = useCallback( + (response: Response) => { + const { status, message } = cortexResponseError(response); + errorToast({ + title: `HTTP Error ${status}`, + message, + }); + }, + [errorToast] + ); + + return errorToastForResponse; +}; From ec86f247d4081beeb9c0b5eb69e3c31aa434bd3d Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Wed, 18 Dec 2024 13:32:40 -0500 Subject: [PATCH 4/5] moar tests, move async stuff to hook --- .../src/components/App.test.tsx | 172 +++++++++++++++- .../src/components/InstructionsModal.tsx | 188 ++---------------- .../src/hooks/cortexHooks.tsx | 175 ++++++++++++++++ 3 files changed, 358 insertions(+), 177 deletions(-) create mode 100644 plugins/pagerduty-incidents/src/hooks/cortexHooks.tsx diff --git a/plugins/pagerduty-incidents/src/components/App.test.tsx b/plugins/pagerduty-incidents/src/components/App.test.tsx index 3e2dc73..4768cad 100644 --- a/plugins/pagerduty-incidents/src/components/App.test.tsx +++ b/plugins/pagerduty-incidents/src/components/App.test.tsx @@ -1,15 +1,15 @@ -import { render, waitFor } from "@testing-library/react"; - +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import fetchMock from "jest-fetch-mock"; - import App from "./App"; +import { act } from "react-dom/test-utils"; + describe("App", () => { beforeEach(() => { // Reset fetchMock before each test to start with a clean slate fetchMock.resetMocks(); }); - it("gets no pd mapping", async () => { + it("does initial configuration when not configured", async () => { const mockBodies = { "https://api.getcortexapp.com/catalog/inventory-planner/openapi": { info: { @@ -25,13 +25,27 @@ describe("App", () => { }, ], }, - "https://api.pagerduty.com/services": { - services: [], + "https://api.getcortexapp.com/plugins/pagerduty-incidents": { + description: + "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html", }, + "https://api.getcortexapp.com/api/internal/v1/secrets": {}, + "https://api.getcortexapp.com/api/internal/v1/proxies": {}, + "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html": + {}, }; fetchMock.mockResponse(async (req) => { const url = req.url.split("?")[0]; + if (url === "https://api.pagerduty.com/abilities") { + // return success if authorization header contains our fake token + if (req.headers.get("Authorization") === "Token token=fake_token_123") { + return { + status: 200, + body: JSON.stringify({ abilities: [] }), + }; + } + } if (!mockBodies[url]) { return { status: 404, @@ -44,11 +58,155 @@ describe("App", () => { }; }); + const { getByText, getByLabelText, getByRole } = render(); + + // Wait for initial text to load + await waitFor(() => { + const element = getByText( + /To configure this plugin automatically, you need a PagerDuty API key/i + ); + expect(element).toBeInTheDocument(); + }); + + // Simulate clicking the "Configure" button to open the modal + const configureButton = screen.getByRole("button", { name: /Configure/i }); + fireEvent.click(configureButton); + + // Simulate typing a fake token into the input field + const tokenInput = getByLabelText(/PagerDuty REST API Token/i); + fireEvent.change(tokenInput, { target: { value: "fake_token_123" } }); + + const submitButton = getByRole("button", { name: "Submit" }); + // Wait for the Submit button to be enabled + await waitFor( + () => { + expect(submitButton).not.toBeDisabled(); + expect(submitButton).toBeVisible(); + }, + { timeout: 1000 } + ); + + await act(async () => { + // Simulate clicking the Submit button + fireEvent.click(submitButton); + }); + + await waitFor( + () => { + const element = getByText(/Configuration completed successfully/i); + expect(element).toBeInTheDocument(); + }, + { timeout: 1000 } + ); + + expect(fetch).toHaveBeenCalledWith("https://api.pagerduty.com/abilities", { + headers: { + Authorization: "Token token=fake_token_123", + }, + }); + }); + + it("gets no PD mapping", async () => { + const mockBodies = { + "https://api.getcortexapp.com/catalog/inventory-planner/openapi": { + info: { + title: "Inventory Planner", + description: "it is a inventory planner", + "x-cortex-tag": "inventory-planner", + "x-cortex-type": "service", + }, + openapi: "3.0.1", + servers: [ + { + url: "/", + }, + ], + }, + "https://api.pagerduty.com/abilities": { + abilities: [], + }, + "https://api.pagerduty.com/services/PXXXXXX": { + service: { + id: "PXXXXXX", + type: "service", + summary: "My Application Service", + self: "https://api.pagerduty.com/services/PXXXXXX", + html_url: "https://subdomain.pagerduty.com/service-directory/PXXXXXX", + name: "My Application Service", + auto_resolve_timeout: 14400, + acknowledgement_timeout: 600, + created_at: "2015-11-06T11:12:51-05:00", + status: "active", + alert_creation: "create_alerts_and_incidents", + integrations: [], + escalation_policy: { + id: "PYYYYYY", + type: "escalation_policy_reference", + summary: "Another Escalation Policy", + self: "https://api.pagerduty.com/escalation_policies/PYYYYYY", + html_url: + "https://subdomain.pagerduty.com/escalation_policies/PYYYYYY", + }, + teams: [], + }, + }, + "https://api.pagerduty.com/oncalls": { + oncalls: [], + }, + "https://api.pagerduty.com/services": { + services: [ + { + id: "PXXXXXX", + type: "service", + summary: "My Application Service", + self: "https://api.pagerduty.com/services/PXXXXXX", + html_url: + "https://subdomain.pagerduty.com/service-directory/PXXXXXX", + name: "My Application Service", + auto_resolve_timeout: 14400, + acknowledgement_timeout: 600, + created_at: "2015-11-06T11:12:51-05:00", + status: "active", + alert_creation: "create_alerts_and_incidents", + integrations: [], + escalation_policy: { + id: "PYYYYYY", + type: "escalation_policy_reference", + summary: "Another Escalation Policy", + self: "https://api.pagerduty.com/escalation_policies/PYYYYYY", + html_url: + "https://subdomain.pagerduty.com/escalation_policies/PYYYYYY", + }, + teams: [], + }, + ], + }, + }; + + fetchMock.mockResponse(async (req) => { + const url = req.url.split("?")[0]; + if (!mockBodies[url]) { + return { + status: 404, + }; + } + const body = mockBodies[url]; + return { + status: 200, + body: JSON.stringify(body), + }; + }); + const { getByText } = render(); await waitFor(() => { - const element = getByText(/Configure PagerDuty Incidents Plugin/); + const element = getByText(/Select a service/); expect(element).toBeInTheDocument(); + expect(fetch).toHaveBeenCalledWith( + expect.stringMatching( + /https:\/\/api\.getcortexapp\.com\/catalog\/inventory-planner\/gitops-logs/ + ) + ); expect(fetch).toHaveBeenCalledWith( expect.stringMatching( /https:\/\/api\.getcortexapp\.com\/catalog\/inventory-planner\/openapi/ diff --git a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx index d069640..6721f81 100644 --- a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx +++ b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx @@ -1,5 +1,5 @@ import type React from "react"; -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Box, @@ -16,10 +16,8 @@ import { Flex, } from "@chakra-ui/react"; -import { usePluginContext } from "@cortexapps/plugin-core/components"; - -import { useErrorToast } from "../hooks/uiHooks"; import { useIsPagerDutyTokenValid } from "../hooks/pagerDutyHooks"; +import { usePluginUpdateFns } from "../hooks/cortexHooks"; interface InstructionsModalProps { isOpen: boolean; @@ -32,20 +30,16 @@ const InstructionsModal: React.FC = ({ onClose, onConfigCompleted, }) => { - const { apiBaseUrl } = usePluginContext(); - - const apiOrigin = useMemo( - () => (apiBaseUrl ? new URL(apiBaseUrl).origin : ""), - [apiBaseUrl] - ); - - const internalBaseUrl = `${apiOrigin}/api/internal/v1`; + const { + doAddSecret, + doAddProxy, + doUpdatePlugin, + } = usePluginUpdateFns(); const [tokenInput, setTokenInput] = useState(""); const [debouncedTokenInput, setDebouncedTokenInput] = useState(""); const tokenIsValid = useIsPagerDutyTokenValid(debouncedTokenInput); - const errorToast = useErrorToast(); const handleInputChange = (e: React.ChangeEvent): void => { setTokenInput(e.target.value); @@ -61,169 +55,21 @@ const InstructionsModal: React.FC = ({ }; }, [tokenInput]); - const doAddSecret = useCallback(async (): Promise => { - console.log("Adding secret"); - const body = JSON.stringify({ - name: "pagerduty_secret", - tag: "pagerduty_secret", - secret: debouncedTokenInput, - }); - - try { - const response = await fetch(`${internalBaseUrl}/secrets`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body, - }); - - if (!response?.ok) { - const message = - (response as any).cortexResponse?.statusText || - response.statusText || - "An error occurred"; - throw new Error(message); - } - return true; - } catch (e) { - console.log("Failed to add secret", e); - let message = e.message || "An error occurred"; - try { - const data = JSON.parse(e.message); - message = data.details || data.message || message; - } catch (e) { - // Ignore - } - errorToast({ - title: "Failed to add secret", - message, - }); - } - return false; - }, [internalBaseUrl, debouncedTokenInput, errorToast]); - - const doAddProxy = useCallback(async (): Promise => { - const body = JSON.stringify({ - name: "PagerDuty Plugin Proxy", - tag: "pagerduty-plugin-proxy", - urlConfigurations: { - "https://api.pagerduty.com": { - urlHeaders: [ - { - name: "Authorization", - value: `Token token={{{secrets.pagerduty_secret}}}`, - }, - ], - }, - }, - }); - - try { - const response = await fetch(`${internalBaseUrl}/proxies`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body, - }); - - if (!response?.ok) { - const message = - (response as any).cortexResponse?.statusText || - response.statusText || - "An error occurred"; - throw new Error(message); - } - return true; - } catch (e) { - console.error("Failed to add proxy", e); - let message = e.message || "An error occurred"; - try { - const data = JSON.parse(e.message); - message = data.details || data.message || message; - } catch (e) { - // Ignore - } - errorToast({ - title: "Failed to add proxy", - message, - }); - } - return false; - }, [internalBaseUrl, errorToast]); - - const doUpdatePlugin = useCallback(async (): Promise => { - try { - let response = await fetch(`${apiBaseUrl}/plugins/pagerduty-incidents`); - if (!response.ok) { - throw new Error("Failed to fetch plugin metadata"); - } - const plugin = await response.json(); - const pluginDescription = plugin.description || ""; - if ( - !pluginDescription.includes( - "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html" - ) - ) { - throw new Error( - "This pagerduty-incidents plugin was not installed by the Plugin Marketplace" - ); - } - response = await fetch( - "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html" - ); - if (!response.ok) { - throw new Error("Failed to fetch plugin UI"); - } - const ui = await response.text(); - plugin.proxyTag = "pagerduty-plugin-proxy"; - plugin.blob = ui; - response = await fetch(`${apiBaseUrl}/plugins/pagerduty-incidents`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(plugin), - }); - if (!response.ok) { - throw new Error("Failed to update plugin"); - } - return true; - } catch (e) { - console.error("Failed to update plugin", e); - let message = e.message || "An error occurred"; - try { - const data = JSON.parse(e.message); - message = data.details || data.message || message; - } catch (e) { - // Ignore - } - errorToast({ - title: "Failed to update plugin", - message, - }); - } - return false; - }, [apiBaseUrl, errorToast]); - - const handleFormSubmit = useCallback(() => { - const handleAsync = async ( - e: React.MouseEvent - ): Promise => { + const handleFormSubmit = useCallback( + async (e: React.MouseEvent) => { e.preventDefault(); const didUpdatePlugin = - (await doAddSecret()) && - (await doAddProxy()) && - (await doUpdatePlugin()); + (await doAddSecret("pagerduty_secret", debouncedTokenInput)) && + (await doAddProxy("PagerDuty Plugin Proxy", "pagerduty-plugin-proxy", "pagerduty_secret")) && + (await doUpdatePlugin("pagerduty-incidents", "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html", "pagerduty-plugin-proxy")); setTokenInput(""); if (didUpdatePlugin) { onConfigCompleted(); } onClose(); - }; - void handleAsync; - }, [onClose, doAddSecret, doAddProxy, doUpdatePlugin, onConfigCompleted]); + }, + [onClose, doAddSecret, doAddProxy, doUpdatePlugin, onConfigCompleted, debouncedTokenInput] + ); return ( = ({ diff --git a/plugins/pagerduty-incidents/src/hooks/cortexHooks.tsx b/plugins/pagerduty-incidents/src/hooks/cortexHooks.tsx new file mode 100644 index 0000000..1db442f --- /dev/null +++ b/plugins/pagerduty-incidents/src/hooks/cortexHooks.tsx @@ -0,0 +1,175 @@ +import { + usePluginContext, +} from "@cortexapps/plugin-core/components"; + +import { useErrorToast } from "./uiHooks"; +import { useCallback } from "react"; + +export const usePluginUpdateFns = () => { + const errorToast = useErrorToast(); + const { apiBaseUrl } = usePluginContext(); + const apiOrigin = apiBaseUrl ? new URL(apiBaseUrl).origin : ""; + const internalBaseUrl = apiOrigin ? `${apiOrigin}/api/internal/v1` : ""; + + const doAddSecret = useCallback(async (name: string, secret: string): Promise => { + if (!internalBaseUrl) { + errorToast({ + title: "Failed to add secret", + message: "Internal base URL not available", + }); + return false; + } + const body = JSON.stringify({ + name, + tag: name, + secret, + }); + + try { + const response = await fetch(`${internalBaseUrl}/secrets`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }); + + if (!response?.ok) { + const message = + (response as any).cortexResponse?.statusText || + response.statusText || + "An error occurred"; + throw new Error(message); + } + return true; + } catch (e) { + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to add secret", + message, + }); + } + return false; + }, [internalBaseUrl, errorToast]); + + const doAddProxy = useCallback(async (name: string, tag: string, secretTag: string): Promise => { + if (!internalBaseUrl) { + errorToast({ + title: "Failed to add proxy", + message: "Internal base URL not available", + }); + return false; + } + const body = JSON.stringify({ + name, + tag, + urlConfigurations: { + "https://api.pagerduty.com": { + urlHeaders: [ + { + name: "Authorization", + value: `Token token={{{secrets.${secretTag}}}}`, + }, + ], + }, + }, + }); + + try { + const response = await fetch(`${internalBaseUrl}/proxies`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }); + + if (!response?.ok) { + const message = + (response as any).cortexResponse?.statusText || + response.statusText || + "An error occurred"; + throw new Error(message); + } + return true; + } catch (e) { + console.error("Failed to add proxy", e); + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to add proxy", + message, + }); + } + return false; + }, [internalBaseUrl, errorToast]); + + const doUpdatePlugin = useCallback(async (pluginTag: string, uiBlobUrl: string, proxyTag: string): Promise => { + try { + let response = await fetch(`${apiBaseUrl}/plugins/${pluginTag}`); + if (!response.ok) { + throw new Error("Failed to fetch plugin metadata"); + } + const plugin = await response.json(); + const pluginDescription = plugin.description || ""; + if ( + !pluginDescription.includes( + "plugin-marketplace" + ) + ) { + throw new Error( + "This pagerduty-incidents plugin was not installed by the Plugin Marketplace" + ); + } + response = await fetch(uiBlobUrl); + if (!response.ok) { + throw new Error("Failed to fetch plugin UI"); + } + const ui = await response.text(); + plugin.proxyTag = proxyTag; + plugin.blob = ui; + response = await fetch(`${apiBaseUrl}/plugins/${pluginTag}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(plugin), + }); + if (!response.ok) { + throw new Error("Failed to update plugin"); + } + return true; + } catch (e) { + console.error("Failed to update plugin", e); + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to update plugin", + message, + }); + } + return false; + }, [apiBaseUrl, errorToast]); + + return { + doAddSecret, + doAddProxy, + doUpdatePlugin, + } +} \ No newline at end of file From bb15108715a2f0a673a852032f0512ec0ab3ce9d Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Wed, 18 Dec 2024 13:34:42 -0500 Subject: [PATCH 5/5] lint --- .../src/components/InstructionsModal.tsx | 27 +- .../src/hooks/cortexHooks.tsx | 309 ++++++++++-------- 2 files changed, 184 insertions(+), 152 deletions(-) diff --git a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx index 6721f81..d0105be 100644 --- a/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx +++ b/plugins/pagerduty-incidents/src/components/InstructionsModal.tsx @@ -30,11 +30,7 @@ const InstructionsModal: React.FC = ({ onClose, onConfigCompleted, }) => { - const { - doAddSecret, - doAddProxy, - doUpdatePlugin, - } = usePluginUpdateFns(); + const { doAddSecret, doAddProxy, doUpdatePlugin } = usePluginUpdateFns(); const [tokenInput, setTokenInput] = useState(""); const [debouncedTokenInput, setDebouncedTokenInput] = useState(""); @@ -60,15 +56,30 @@ const InstructionsModal: React.FC = ({ e.preventDefault(); const didUpdatePlugin = (await doAddSecret("pagerduty_secret", debouncedTokenInput)) && - (await doAddProxy("PagerDuty Plugin Proxy", "pagerduty-plugin-proxy", "pagerduty_secret")) && - (await doUpdatePlugin("pagerduty-incidents", "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html", "pagerduty-plugin-proxy")); + (await doAddProxy( + "PagerDuty Plugin Proxy", + "pagerduty-plugin-proxy", + "pagerduty_secret" + )) && + (await doUpdatePlugin( + "pagerduty-incidents", + "https://plugin-marketplace.s3.us-east-2.amazonaws.com/pagerduty-plugin/ui.html", + "pagerduty-plugin-proxy" + )); setTokenInput(""); if (didUpdatePlugin) { onConfigCompleted(); } onClose(); }, - [onClose, doAddSecret, doAddProxy, doUpdatePlugin, onConfigCompleted, debouncedTokenInput] + [ + onClose, + doAddSecret, + doAddProxy, + doUpdatePlugin, + onConfigCompleted, + debouncedTokenInput, + ] ); return ( diff --git a/plugins/pagerduty-incidents/src/hooks/cortexHooks.tsx b/plugins/pagerduty-incidents/src/hooks/cortexHooks.tsx index 1db442f..8f5a906 100644 --- a/plugins/pagerduty-incidents/src/hooks/cortexHooks.tsx +++ b/plugins/pagerduty-incidents/src/hooks/cortexHooks.tsx @@ -1,175 +1,196 @@ -import { - usePluginContext, -} from "@cortexapps/plugin-core/components"; +import { usePluginContext } from "@cortexapps/plugin-core/components"; import { useErrorToast } from "./uiHooks"; import { useCallback } from "react"; -export const usePluginUpdateFns = () => { +export interface PluginUpdateFns { + doAddSecret: (name: string, secret: string) => Promise; + doAddProxy: ( + name: string, + tag: string, + secretTag: string + ) => Promise; + doUpdatePlugin: ( + pluginTag: string, + uiBlobUrl: string, + proxyTag: string + ) => Promise; +} + +export const usePluginUpdateFns = (): PluginUpdateFns => { const errorToast = useErrorToast(); const { apiBaseUrl } = usePluginContext(); const apiOrigin = apiBaseUrl ? new URL(apiBaseUrl).origin : ""; const internalBaseUrl = apiOrigin ? `${apiOrigin}/api/internal/v1` : ""; - const doAddSecret = useCallback(async (name: string, secret: string): Promise => { - if (!internalBaseUrl) { - errorToast({ - title: "Failed to add secret", - message: "Internal base URL not available", - }); - return false; - } - const body = JSON.stringify({ - name, - tag: name, - secret, - }); - - try { - const response = await fetch(`${internalBaseUrl}/secrets`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body, + const doAddSecret = useCallback( + async (name: string, secret: string): Promise => { + if (!internalBaseUrl) { + errorToast({ + title: "Failed to add secret", + message: "Internal base URL not available", + }); + return false; + } + const body = JSON.stringify({ + name, + tag: name, + secret, }); - if (!response?.ok) { - const message = - (response as any).cortexResponse?.statusText || - response.statusText || - "An error occurred"; - throw new Error(message); - } - return true; - } catch (e) { - let message = e.message || "An error occurred"; try { - const data = JSON.parse(e.message); - message = data.details || data.message || message; + const response = await fetch(`${internalBaseUrl}/secrets`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }); + + if (!response?.ok) { + const message = + (response as any).cortexResponse?.statusText || + response.statusText || + "An error occurred"; + throw new Error(message); + } + return true; } catch (e) { - // Ignore + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to add secret", + message, + }); } - errorToast({ - title: "Failed to add secret", - message, - }); - } - return false; - }, [internalBaseUrl, errorToast]); - - const doAddProxy = useCallback(async (name: string, tag: string, secretTag: string): Promise => { - if (!internalBaseUrl) { - errorToast({ - title: "Failed to add proxy", - message: "Internal base URL not available", - }); return false; - } - const body = JSON.stringify({ - name, - tag, - urlConfigurations: { - "https://api.pagerduty.com": { - urlHeaders: [ - { - name: "Authorization", - value: `Token token={{{secrets.${secretTag}}}}`, - }, - ], - }, - }, - }); + }, + [internalBaseUrl, errorToast] + ); - try { - const response = await fetch(`${internalBaseUrl}/proxies`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const doAddProxy = useCallback( + async (name: string, tag: string, secretTag: string): Promise => { + if (!internalBaseUrl) { + errorToast({ + title: "Failed to add proxy", + message: "Internal base URL not available", + }); + return false; + } + const body = JSON.stringify({ + name, + tag, + urlConfigurations: { + "https://api.pagerduty.com": { + urlHeaders: [ + { + name: "Authorization", + value: `Token token={{{secrets.${secretTag}}}}`, + }, + ], + }, }, - body, }); - if (!response?.ok) { - const message = - (response as any).cortexResponse?.statusText || - response.statusText || - "An error occurred"; - throw new Error(message); - } - return true; - } catch (e) { - console.error("Failed to add proxy", e); - let message = e.message || "An error occurred"; try { - const data = JSON.parse(e.message); - message = data.details || data.message || message; + const response = await fetch(`${internalBaseUrl}/proxies`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }); + + if (!response?.ok) { + const message = + (response as any).cortexResponse?.statusText || + response.statusText || + "An error occurred"; + throw new Error(message); + } + return true; } catch (e) { - // Ignore + console.error("Failed to add proxy", e); + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to add proxy", + message, + }); } - errorToast({ - title: "Failed to add proxy", - message, - }); - } - return false; - }, [internalBaseUrl, errorToast]); + return false; + }, + [internalBaseUrl, errorToast] + ); - const doUpdatePlugin = useCallback(async (pluginTag: string, uiBlobUrl: string, proxyTag: string): Promise => { - try { - let response = await fetch(`${apiBaseUrl}/plugins/${pluginTag}`); - if (!response.ok) { - throw new Error("Failed to fetch plugin metadata"); - } - const plugin = await response.json(); - const pluginDescription = plugin.description || ""; - if ( - !pluginDescription.includes( - "plugin-marketplace" - ) - ) { - throw new Error( - "This pagerduty-incidents plugin was not installed by the Plugin Marketplace" - ); - } - response = await fetch(uiBlobUrl); - if (!response.ok) { - throw new Error("Failed to fetch plugin UI"); - } - const ui = await response.text(); - plugin.proxyTag = proxyTag; - plugin.blob = ui; - response = await fetch(`${apiBaseUrl}/plugins/${pluginTag}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(plugin), - }); - if (!response.ok) { - throw new Error("Failed to update plugin"); - } - return true; - } catch (e) { - console.error("Failed to update plugin", e); - let message = e.message || "An error occurred"; + const doUpdatePlugin = useCallback( + async ( + pluginTag: string, + uiBlobUrl: string, + proxyTag: string + ): Promise => { try { - const data = JSON.parse(e.message); - message = data.details || data.message || message; + let response = await fetch(`${apiBaseUrl}/plugins/${pluginTag}`); + if (!response.ok) { + throw new Error("Failed to fetch plugin metadata"); + } + const plugin = await response.json(); + const pluginDescription = plugin.description || ""; + if (!pluginDescription.includes("plugin-marketplace")) { + throw new Error( + "This pagerduty-incidents plugin was not installed by the Plugin Marketplace" + ); + } + response = await fetch(uiBlobUrl); + if (!response.ok) { + throw new Error("Failed to fetch plugin UI"); + } + const ui = await response.text(); + plugin.proxyTag = proxyTag; + plugin.blob = ui; + response = await fetch(`${apiBaseUrl}/plugins/${pluginTag}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(plugin), + }); + if (!response.ok) { + throw new Error("Failed to update plugin"); + } + return true; } catch (e) { - // Ignore + console.error("Failed to update plugin", e); + let message = e.message || "An error occurred"; + try { + const data = JSON.parse(e.message); + message = data.details || data.message || message; + } catch (e) { + // Ignore + } + errorToast({ + title: "Failed to update plugin", + message, + }); } - errorToast({ - title: "Failed to update plugin", - message, - }); - } - return false; - }, [apiBaseUrl, errorToast]); + return false; + }, + [apiBaseUrl, errorToast] + ); return { doAddSecret, doAddProxy, doUpdatePlugin, - } -} \ No newline at end of file + }; +};