diff --git a/plugins/info-cards/README.md b/plugins/info-cards/README.md
index 0ba2a0a..1c02f1a 100644
--- a/plugins/info-cards/README.md
+++ b/plugins/info-cards/README.md
@@ -1,4 +1,27 @@
-# 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 +45,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/).
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..30ed734 100644
--- a/plugins/info-cards/src/components/App.tsx
+++ b/plugins/info-cards/src/components/App.tsx
@@ -2,21 +2,30 @@ 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/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) => (
))}
diff --git a/plugins/info-cards/src/components/PluginRoot.tsx b/plugins/info-cards/src/components/PluginRoot.tsx
index 38ea87b..a739d5a 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();
@@ -37,16 +41,21 @@ 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 {
- await savePluginConfig({
+ savePluginConfig({
...pluginConfig,
info: {
...pluginConfig?.info,
"x-cortex-definition": {
...(pluginConfig?.info?.["x-cortex-definition"] || {}),
- infoRows,
+ infoRows: [...infoRows],
},
},
});
@@ -71,16 +80,12 @@ export default function PluginRoot(): JSX.Element {
};
void doSave();
- }, [infoRows, pluginConfig, savePluginConfig, toast]);
+ }, [infoRows, pluginConfig, savePluginConfig, toast, toggleEditor]);
- if (configIsLoading) {
+ if (configIsLoading || configIsFetching || configIsMutating) {
return ;
}
- const toggleEditor = (): void => {
- setIsEditorPage((prev) => !prev);
- };
-
return (
<>
{isModified && (
diff --git a/plugins/info-cards/src/hooks.tsx b/plugins/info-cards/src/hooks.tsx
index 6075cf1..c50f4b5 100644
--- a/plugins/info-cards/src/hooks.tsx
+++ b/plugins/info-cards/src/hooks.tsx
@@ -1,126 +1,98 @@
-import { useCallback, useEffect, useState } from "react";
-import YAML from "yaml";
+import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
+
+import type { UseMutationResult } from "@tanstack/react-query";
import { usePluginContext } from "@cortexapps/plugin-core/components";
-export interface UsePluginConfigReturn {
+export interface UseEntityDescriptorProps {
+ entityTag: string;
+ mutationMethod?: "PATCH" | "POST";
+ 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;
- pluginConfig: any | null;
- savePluginConfig: (config: any) => Promise;
- refreshPluginConfig: () => void;
+ isFetching: boolean;
+ error: unknown;
+ updateEntity: UseMutationResult["mutate"];
+ isMutating: boolean;
}
-export const usePluginConfig = (): UsePluginConfigReturn => {
+export const useEntityDescriptor = ({
+ entityTag,
+ mutationMethod = "POST",
+ onMutateSuccess = () => {},
+ onMutateError = () => {},
+ onMutateSettled = () => {},
+ onMutate = () => {},
+}: UseEntityDescriptorProps): UseEntityDescriptorReturn => {
const { apiBaseUrl } = usePluginContext();
- const [refreshCounter, setRefreshCounter] = useState(0);
- const [isLoading, setIsLoading] = useState(true);
- const [pluginConfig, setPluginConfig] = useState(null);
-
- 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;
+ const queryClient = useQueryClient();
- // 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 await 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",
+ const response = await fetch(`${apiBaseUrl}/open-api`, {
+ method: mutationMethod,
headers: {
"Content-Type": "application/openapi;charset=utf-8",
},
- body: YAML.stringify(config),
+ body: JSON.stringify(data),
});
-
- setRefreshCounter((prev) => prev + 1);
+ return await response.json();
},
- [apiBaseUrl]
- );
-
- const refreshPluginConfig = useCallback(() => {
- setRefreshCounter((prev) => prev + 1);
- }, []);
+ onMutate,
+ onError: onMutateError,
+ onSettled: onMutateSettled,
+ onSuccess: (data, variables, context) => {
+ void queryClient.invalidateQueries({
+ queryKey: ["entityDescriptor", entityTag],
+ });
+ onMutateSuccess(data, variables, context);
+ },
+ });
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"