From d9c9a55ce87c99683a39363eb21ef28a6ed2b320 Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Thu, 30 Jan 2025 23:31:56 -0500 Subject: [PATCH 1/6] trying react query --- plugins/info-cards/package.json | 2 + plugins/info-cards/src/components/App.tsx | 24 ++- .../info-cards/src/components/PluginRoot.tsx | 14 +- plugins/info-cards/src/hooks.tsx | 181 +++++++----------- yarn.lock | 24 +++ 5 files changed, 124 insertions(+), 121 deletions(-) diff --git a/plugins/info-cards/package.json b/plugins/info-cards/package.json index ad4c3a0..6ca5fe2 100644 --- a/plugins/info-cards/package.json +++ b/plugins/info-cards/package.json @@ -11,6 +11,8 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@nikolovlazar/chakra-ui-prose": "^1.2.1", + "@tanstack/react-query": "^5.65.1", + "@tanstack/react-query-devtools": "^5.65.1", "@uiw/react-codemirror": "^4.23.7", "dompurify": "^3.2.3", "framer-motion": "^11.13.5", diff --git a/plugins/info-cards/src/components/App.tsx b/plugins/info-cards/src/components/App.tsx index 8389cad..081817a 100644 --- a/plugins/info-cards/src/components/App.tsx +++ b/plugins/info-cards/src/components/App.tsx @@ -2,21 +2,33 @@ import type React from "react"; import { PluginProvider } from "@cortexapps/plugin-core/components"; import { ChakraProvider } from "@chakra-ui/react"; +import { + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; + +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + import "../baseStyles.css"; import ErrorBoundary from "./ErrorBoundary"; import PluginRoot from "./PluginRoot"; import theme from "./ui/theme"; const App: React.FC = () => { + const queryClient = new QueryClient(); return ( - - - + + + + {/* ReactQueryDevTools will only show in dev server */} + + + ); diff --git a/plugins/info-cards/src/components/PluginRoot.tsx b/plugins/info-cards/src/components/PluginRoot.tsx index 38ea87b..45c892f 100644 --- a/plugins/info-cards/src/components/PluginRoot.tsx +++ b/plugins/info-cards/src/components/PluginRoot.tsx @@ -9,7 +9,7 @@ import type { InfoRowI } from "../typings"; import LandingPage from "./LandingPage"; import LayoutBuilder from "./LayoutBuilder"; -import { usePluginConfig } from "../hooks"; +import { useEntityDescriptor } from "../hooks"; export default function PluginRoot(): JSX.Element { const [isEditorPage, setIsEditorPage] = useState(false); @@ -17,9 +17,13 @@ export default function PluginRoot(): JSX.Element { const { isLoading: configIsLoading, - pluginConfig, - savePluginConfig, - } = usePluginConfig(); + isFetching: configIsFetching, + isMutating: configIsMutating, + entity: pluginConfig, + updateEntity: savePluginConfig, + } = useEntityDescriptor({ + entityTag: "info-cards-plugin-config", + }); const toast = useToast(); @@ -73,7 +77,7 @@ export default function PluginRoot(): JSX.Element { void doSave(); }, [infoRows, pluginConfig, savePluginConfig, toast]); - if (configIsLoading) { + if (configIsLoading || configIsFetching || configIsMutating) { return ; } diff --git a/plugins/info-cards/src/hooks.tsx b/plugins/info-cards/src/hooks.tsx index 6075cf1..da547d0 100644 --- a/plugins/info-cards/src/hooks.tsx +++ b/plugins/info-cards/src/hooks.tsx @@ -1,126 +1,87 @@ -import { useCallback, useEffect, useState } from "react"; -import YAML from "yaml"; +import { + useQueryClient, + useQuery, + useMutation, +} from "@tanstack/react-query"; import { usePluginContext } from "@cortexapps/plugin-core/components"; -export interface UsePluginConfigReturn { - isLoading: boolean; - pluginConfig: any | null; - savePluginConfig: (config: any) => Promise; - refreshPluginConfig: () => void; +export interface UseEntityDescriptorProps { + entityTag: string; + mutationMethod?: "PATCH" | "POST"; + onMutateSuccess?: (data: any, variables: any, context: void) => void; + onMutateError?: (error: Error, variables: any, context: void) => void; + onMutateSettled?: (data: any, error: Error, variables: any, context: void) => void; + onMutate?: (variables: any) => void; } -export const usePluginConfig = (): UsePluginConfigReturn => { +export const useEntityDescriptor = ({ + entityTag, + mutationMethod = "POST", + onMutateSuccess = () => {}, + onMutateError = () => {}, + onMutateSettled = () => {}, + onMutate = () => {}, +}: UseEntityDescriptorProps) => { const { apiBaseUrl } = usePluginContext(); - const [refreshCounter, setRefreshCounter] = useState(0); - const [isLoading, setIsLoading] = useState(true); - const [pluginConfig, setPluginConfig] = useState(null); + const queryClient = useQueryClient(); - useEffect(() => { - const fetchPluginConfig = async (): Promise => { - setIsLoading(true); - setPluginConfig(null); - try { - const response = await fetch( - `${apiBaseUrl}/catalog/info-cards-plugin-config/openapi` - ); - const config = await response.json(); - setPluginConfig(config); - } catch (error) { - console.error(error); - } finally { - setIsLoading(false); - } - }; - void fetchPluginConfig(); - }, [apiBaseUrl, refreshCounter]); - - const savePluginConfig = useCallback( - async (config: any) => { - let existingConfig: any; - - // Fetch existing config if it exists - try { - const r = await fetch( - `${apiBaseUrl}/catalog/info-cards-plugin-config/openapi` - ); - if (!r.ok) { - throw new Error("Failed to fetch existing config"); - } - existingConfig = await r.json(); - } catch (error) {} + const query = useQuery({ + queryKey: ["entityDescriptor", entityTag], + queryFn: async () => { + const response = await fetch( + `${apiBaseUrl}/catalog/${entityTag}/openapi` + ); + return response.json(); + }, + enabled: !!apiBaseUrl, + retry: false, + }); - // Validate the passed in config - if (!config.info?.["x-cortex-definition"]?.infoRows) { - // this should never happen since the plugin should always pass in a valid config - console.error("Invalid config", config); - throw new Error("Invalid config"); + const mutation = useMutation({ + mutationFn: async (data: any) => { + // throw if the data is not an object or data.info is not an object + if (typeof data !== "object" || typeof data.info !== "object") { + throw new Error("Invalid data format"); } - - config.info["x-cortex-tag"] = "info-cards-plugin-config"; - config.info.title = "Info Cards Plugin Configuration"; - config.openapi = "3.0.1"; - - // Preserve the existing x-cortex-type if it exists - config.info["x-cortex-type"] = - existingConfig?.info?.["x-cortex-type"] || "plugin-configuration"; - - // See if the entity type exists, if not create it - try { - const r = await fetch( - `${apiBaseUrl}/catalog/definitions/${ - config.info["x-cortex-type"] as string - }` - ); - if (!r.ok) { - throw new Error("Failed to fetch existing entity type"); - } - } catch (error) { - // Create the entity type - const entityTypeBody = { - iconTag: "bucket", - name: "Plugin Configuration", - schema: { properties: {}, required: [] }, - type: config.info["x-cortex-type"], - }; - const entityTypeResponse = await fetch( - `${apiBaseUrl}/catalog/definitions`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(entityTypeBody), - } - ); - if (!entityTypeResponse.ok) { - throw new Error("Failed to create entity type"); - } + // make sure basic info is set + data.openapi = "3.0.1"; + // don't allow changing the tag + data.info["x-cortex-tag"] = entityTag; + // set a title if it's not set + if (!data.info.title) { + data.info.title = entityTag.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); } - - // Save the new config - await fetch(`${apiBaseUrl}/open-api`, { - method: "POST", - headers: { - "Content-Type": "application/openapi;charset=utf-8", - }, - body: YAML.stringify(config), + const response = await fetch( + `${apiBaseUrl}/open-api`, + { + method: mutationMethod, + headers: { + "Content-Type": "application/openapi;charset=utf-8", + }, + body: JSON.stringify(data), + } + ); + return response.json(); + }, + onMutate: onMutate, + onError: onMutateError, + onSettled: onMutateSettled, + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: ["entityDescriptor", entityTag], }); - - setRefreshCounter((prev) => prev + 1); + onMutateSuccess(data, variables, context); }, - [apiBaseUrl] - ); - - const refreshPluginConfig = useCallback(() => { - setRefreshCounter((prev) => prev + 1); - }, []); + }); return { - isLoading, - pluginConfig, - savePluginConfig, - refreshPluginConfig, - }; + entity: query.data, + isLoading: query.isLoading, + isFetching: query.isFetching, + error: query.error, + updateEntity: mutation.mutate, + isMutating: mutation.isPending, + } }; diff --git a/yarn.lock b/yarn.lock index 7a815c1..a7af243 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3742,6 +3742,30 @@ resolved "https://registry.npmjs.org/@swc/types/-/types-0.1.4.tgz" integrity sha512-z/G02d+59gyyUb7KYhKi9jOhicek6QD2oMaotUyG+lUkybpXoV49dY9bj7Ah5Q+y7knK2jU67UTX9FyfGzaxQg== +"@tanstack/query-core@5.65.0": + version "5.65.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.65.0.tgz#6b7c7087a36867361535b613ff39b633808052fd" + integrity sha512-Bnnq/1axf00r2grRT6gUyIkZRKzhHs+p4DijrCQ3wMlA3D3TTT71gtaSLtqnzGddj73/7X5JDGyjiSLdjvQN4w== + +"@tanstack/query-devtools@5.65.0": + version "5.65.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz#37da5e911543b4f6d98b9a04369eab0de6044ba1" + integrity sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg== + +"@tanstack/react-query-devtools@^5.65.1": + version "5.65.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.65.1.tgz#375ba44398076436f9c82f5ae46f0a3d47397db6" + integrity sha512-PKUBz7+FAP3eI1zoWrP5vxNQXs+elPz3u/3cILGhNZl2dufgbU9OJRpbC+BAptLXTsGxTwkAlrWBIZbD/c7CDw== + dependencies: + "@tanstack/query-devtools" "5.65.0" + +"@tanstack/react-query@^5.65.1": + version "5.65.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.65.1.tgz#84f120f7cf7398626c991ccef4557bd4d780fe37" + integrity sha512-BSpjo4RQdJ75Mw3pqM1AJYNhanNxJE3ct7RmCZUAv9cUJg/Qmonzc/Xy2kKXeQA1InuKATSuc6pOZciWOF8TYQ== + dependencies: + "@tanstack/query-core" "5.65.0" + "@testing-library/dom@^9.0.0": version "9.3.1" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz" From 0bee360fa7aa9bde45d1fbe5e0fb13135d305a7e Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Mon, 3 Mar 2025 23:09:48 -0500 Subject: [PATCH 2/6] update README --- plugins/info-cards/README.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/plugins/info-cards/README.md b/plugins/info-cards/README.md index 0ba2a0a..744ee0f 100644 --- a/plugins/info-cards/README.md +++ b/plugins/info-cards/README.md @@ -1,4 +1,26 @@ -# Example +# Info Cards Plugin + +The Info Cards plugin provides an easy way to display useful links and information in the global context, with a user-friendly layout editor. + +## Installation + +Install the plugin via the Cortex Plugins Marketplace or manually by adding it to your Cortex instance using the notes under Manual installation below. + +## Configuration + +No manual configuration is required after installation. + +### Saving and Editing Layouts +- When a card layout is saved for the first time, it will be stored in an entity with the tag `info-cards-plugin-config`. +- To edit an existing layout, click the small pencil icon in the top right corner of the plugin area. + +## Usage + +1. Open the Info Cards plugin from the global context. +2. Add or edit cards using the layout editor. +3. Save your changes to persist the layout. + +# Manual installation Info Cards Plugin is a [Cortex](https://www.cortex.io/) plugin. To see how to run the plugin inside of Cortex, see [our docs](https://docs.cortex.io/docs/plugins). @@ -22,7 +44,3 @@ The following commands come pre-configured in this repository. You can see all a - `lint` - runs lint and format checking on the repository using [prettier](https://prettier.io/) and [eslint](https://eslint.org/) - `lintfix` - runs eslint in fix mode to fix any linting errors that can be fixed automatically - `formatfix` - runs Prettier in fix mode to fix any formatting errors that can be fixed automatically - -### Available React components - -See available UI components via our [Storybook](https://cortexapps.github.io/plugin-core/). From cc1b88c1360d3fb2d808df2e30df9652353d2f10 Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Mon, 3 Mar 2025 23:40:29 -0500 Subject: [PATCH 3/6] format and lint --- plugins/info-cards/README.md | 1 + plugins/info-cards/src/components/App.tsx | 7 +-- .../info-cards/src/components/PluginRoot.tsx | 2 +- plugins/info-cards/src/hooks.tsx | 61 +++++++++++-------- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/plugins/info-cards/README.md b/plugins/info-cards/README.md index 744ee0f..1c02f1a 100644 --- a/plugins/info-cards/README.md +++ b/plugins/info-cards/README.md @@ -11,6 +11,7 @@ Install the plugin via the Cortex Plugins Marketplace or manually by adding it t No manual configuration is required after installation. ### Saving and Editing Layouts + - When a card layout is saved for the first time, it will be stored in an entity with the tag `info-cards-plugin-config`. - To edit an existing layout, click the small pencil icon in the top right corner of the plugin area. diff --git a/plugins/info-cards/src/components/App.tsx b/plugins/info-cards/src/components/App.tsx index 081817a..30ed734 100644 --- a/plugins/info-cards/src/components/App.tsx +++ b/plugins/info-cards/src/components/App.tsx @@ -2,12 +2,9 @@ import type React from "react"; import { PluginProvider } from "@cortexapps/plugin-core/components"; import { ChakraProvider } from "@chakra-ui/react"; -import { - QueryClient, - QueryClientProvider, -} from "@tanstack/react-query"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import "../baseStyles.css"; import ErrorBoundary from "./ErrorBoundary"; diff --git a/plugins/info-cards/src/components/PluginRoot.tsx b/plugins/info-cards/src/components/PluginRoot.tsx index 45c892f..dffe513 100644 --- a/plugins/info-cards/src/components/PluginRoot.tsx +++ b/plugins/info-cards/src/components/PluginRoot.tsx @@ -44,7 +44,7 @@ export default function PluginRoot(): JSX.Element { const handleSubmit = useCallback(() => { const doSave = async (): Promise => { try { - await savePluginConfig({ + savePluginConfig({ ...pluginConfig, info: { ...pluginConfig?.info, diff --git a/plugins/info-cards/src/hooks.tsx b/plugins/info-cards/src/hooks.tsx index da547d0..c50f4b5 100644 --- a/plugins/info-cards/src/hooks.tsx +++ b/plugins/info-cards/src/hooks.tsx @@ -1,20 +1,32 @@ -import { - useQueryClient, - useQuery, - useMutation, -} from "@tanstack/react-query"; +import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query"; + +import type { UseMutationResult } from "@tanstack/react-query"; import { usePluginContext } from "@cortexapps/plugin-core/components"; export interface UseEntityDescriptorProps { entityTag: string; mutationMethod?: "PATCH" | "POST"; - onMutateSuccess?: (data: any, variables: any, context: void) => void; - onMutateError?: (error: Error, variables: any, context: void) => void; - onMutateSettled?: (data: any, error: Error, variables: any, context: void) => void; + onMutateSuccess?: (data: any, variables: any, context?: any) => void; + onMutateError?: (error: Error, variables: any, context?: any) => void; + onMutateSettled?: ( + data: any, + error: Error, + variables: any, + context?: any + ) => void; onMutate?: (variables: any) => void; } +export interface UseEntityDescriptorReturn { + entity: any; + isLoading: boolean; + isFetching: boolean; + error: unknown; + updateEntity: UseMutationResult["mutate"]; + isMutating: boolean; +} + export const useEntityDescriptor = ({ entityTag, mutationMethod = "POST", @@ -22,7 +34,7 @@ export const useEntityDescriptor = ({ onMutateError = () => {}, onMutateSettled = () => {}, onMutate = () => {}, -}: UseEntityDescriptorProps) => { +}: UseEntityDescriptorProps): UseEntityDescriptorReturn => { const { apiBaseUrl } = usePluginContext(); const queryClient = useQueryClient(); @@ -33,7 +45,7 @@ export const useEntityDescriptor = ({ const response = await fetch( `${apiBaseUrl}/catalog/${entityTag}/openapi` ); - return response.json(); + return await response.json(); }, enabled: !!apiBaseUrl, retry: false, @@ -51,25 +63,24 @@ export const useEntityDescriptor = ({ data.info["x-cortex-tag"] = entityTag; // set a title if it's not set if (!data.info.title) { - data.info.title = entityTag.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + data.info.title = entityTag + .replace(/-/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase()); } - const response = await fetch( - `${apiBaseUrl}/open-api`, - { - method: mutationMethod, - headers: { - "Content-Type": "application/openapi;charset=utf-8", - }, - body: JSON.stringify(data), - } - ); - return response.json(); + const response = await fetch(`${apiBaseUrl}/open-api`, { + method: mutationMethod, + headers: { + "Content-Type": "application/openapi;charset=utf-8", + }, + body: JSON.stringify(data), + }); + return await response.json(); }, - onMutate: onMutate, + onMutate, onError: onMutateError, onSettled: onMutateSettled, onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ + void queryClient.invalidateQueries({ queryKey: ["entityDescriptor", entityTag], }); onMutateSuccess(data, variables, context); @@ -83,5 +94,5 @@ export const useEntityDescriptor = ({ error: query.error, updateEntity: mutation.mutate, isMutating: mutation.isPending, - } + }; }; From 4addd8b464672d430b181d19aee02aa71fb55e10 Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Wed, 5 Mar 2025 09:23:47 -0500 Subject: [PATCH 4/6] fix cancel behavior --- plugins/info-cards/src/components/PluginRoot.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/info-cards/src/components/PluginRoot.tsx b/plugins/info-cards/src/components/PluginRoot.tsx index dffe513..7d7831b 100644 --- a/plugins/info-cards/src/components/PluginRoot.tsx +++ b/plugins/info-cards/src/components/PluginRoot.tsx @@ -50,7 +50,7 @@ export default function PluginRoot(): JSX.Element { ...pluginConfig?.info, "x-cortex-definition": { ...(pluginConfig?.info?.["x-cortex-definition"] || {}), - infoRows, + infoRows: [...infoRows], }, }, }); @@ -82,6 +82,7 @@ export default function PluginRoot(): JSX.Element { } const toggleEditor = (): void => { + setInfoRows(pluginConfig?.info?.["x-cortex-definition"]?.infoRows || []); setIsEditorPage((prev) => !prev); }; From 07094dad74adefcf7ba13886fa2300a3ca8037f1 Mon Sep 17 00:00:00 2001 From: Cafer Elgin Date: Wed, 5 Mar 2025 17:21:03 +0200 Subject: [PATCH 5/6] feat: empty row acts as divider --- plugins/info-cards/src/components/InfoRow.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/info-cards/src/components/InfoRow.tsx b/plugins/info-cards/src/components/InfoRow.tsx index 5554e34..323064f 100644 --- a/plugins/info-cards/src/components/InfoRow.tsx +++ b/plugins/info-cards/src/components/InfoRow.tsx @@ -1,5 +1,5 @@ import type { InfoRowI } from "../typings"; -import { Box } from "@chakra-ui/react"; +import { Box, theme } from "@chakra-ui/react"; import InfoCard from "./InfoCard"; interface InfoRowProps { @@ -7,7 +7,17 @@ interface InfoRowProps { } export default function InfoRow({ infoRow }: InfoRowProps): JSX.Element { return ( - + {infoRow.cards.map((card) => ( ))} From d8d3bd83187e34324d2fd4df7cf8e3fef54b3004 Mon Sep 17 00:00:00 2001 From: Martin Stone Date: Wed, 5 Mar 2025 11:43:15 -0500 Subject: [PATCH 6/6] fix lint --- plugins/info-cards/src/components/PluginRoot.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/info-cards/src/components/PluginRoot.tsx b/plugins/info-cards/src/components/PluginRoot.tsx index 7d7831b..a739d5a 100644 --- a/plugins/info-cards/src/components/PluginRoot.tsx +++ b/plugins/info-cards/src/components/PluginRoot.tsx @@ -41,6 +41,11 @@ export default function PluginRoot(): JSX.Element { return Boolean(isModified); }, [infoRows, pluginConfig]); + const toggleEditor = useCallback(() => { + setInfoRows(pluginConfig?.info?.["x-cortex-definition"]?.infoRows || []); + setIsEditorPage((prev) => !prev); + }, [pluginConfig, setInfoRows, setIsEditorPage]); + const handleSubmit = useCallback(() => { const doSave = async (): Promise => { try { @@ -75,17 +80,12 @@ export default function PluginRoot(): JSX.Element { }; void doSave(); - }, [infoRows, pluginConfig, savePluginConfig, toast]); + }, [infoRows, pluginConfig, savePluginConfig, toast, toggleEditor]); if (configIsLoading || configIsFetching || configIsMutating) { return ; } - const toggleEditor = (): void => { - setInfoRows(pluginConfig?.info?.["x-cortex-definition"]?.infoRows || []); - setIsEditorPage((prev) => !prev); - }; - return ( <> {isModified && (