Skip to content

Commit 2e04827

Browse files
Info cards react query (#45)
Co-authored-by: Cafer Elgin <[email protected]>
1 parent 9740d65 commit 2e04827

File tree

7 files changed

+171
-130
lines changed

7 files changed

+171
-130
lines changed

plugins/info-cards/README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
1-
# Example
1+
# Info Cards Plugin
2+
3+
The Info Cards plugin provides an easy way to display useful links and information in the global context, with a user-friendly layout editor.
4+
5+
## Installation
6+
7+
Install the plugin via the Cortex Plugins Marketplace or manually by adding it to your Cortex instance using the notes under Manual installation below.
8+
9+
## Configuration
10+
11+
No manual configuration is required after installation.
12+
13+
### Saving and Editing Layouts
14+
15+
- When a card layout is saved for the first time, it will be stored in an entity with the tag `info-cards-plugin-config`.
16+
- To edit an existing layout, click the small pencil icon in the top right corner of the plugin area.
17+
18+
## Usage
19+
20+
1. Open the Info Cards plugin from the global context.
21+
2. Add or edit cards using the layout editor.
22+
3. Save your changes to persist the layout.
23+
24+
# Manual installation
225

326
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).
427

@@ -22,7 +45,3 @@ The following commands come pre-configured in this repository. You can see all a
2245
- `lint` - runs lint and format checking on the repository using [prettier](https://prettier.io/) and [eslint](https://eslint.org/)
2346
- `lintfix` - runs eslint in fix mode to fix any linting errors that can be fixed automatically
2447
- `formatfix` - runs Prettier in fix mode to fix any formatting errors that can be fixed automatically
25-
26-
### Available React components
27-
28-
See available UI components via our [Storybook](https://cortexapps.github.io/plugin-core/).

plugins/info-cards/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"@emotion/react": "^11.14.0",
1212
"@emotion/styled": "^11.14.0",
1313
"@nikolovlazar/chakra-ui-prose": "^1.2.1",
14+
"@tanstack/react-query": "^5.65.1",
15+
"@tanstack/react-query-devtools": "^5.65.1",
1416
"@uiw/react-codemirror": "^4.23.7",
1517
"dompurify": "^3.2.3",
1618
"framer-motion": "^11.13.5",

plugins/info-cards/src/components/App.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@ import type React from "react";
22
import { PluginProvider } from "@cortexapps/plugin-core/components";
33
import { ChakraProvider } from "@chakra-ui/react";
44

5+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6+
7+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
8+
59
import "../baseStyles.css";
610
import ErrorBoundary from "./ErrorBoundary";
711
import PluginRoot from "./PluginRoot";
812
import theme from "./ui/theme";
913

1014
const App: React.FC = () => {
15+
const queryClient = new QueryClient();
1116
return (
1217
<ErrorBoundary>
1318
<PluginProvider>
14-
<ChakraProvider
15-
theme={theme}
16-
toastOptions={{ defaultOptions: { position: "top" } }}
17-
>
18-
<PluginRoot />
19-
</ChakraProvider>
19+
<QueryClientProvider client={queryClient}>
20+
<ChakraProvider
21+
theme={theme}
22+
toastOptions={{ defaultOptions: { position: "top" } }}
23+
>
24+
<PluginRoot />
25+
{/* ReactQueryDevTools will only show in dev server */}
26+
<ReactQueryDevtools initialIsOpen={false} />
27+
</ChakraProvider>
28+
</QueryClientProvider>
2029
</PluginProvider>
2130
</ErrorBoundary>
2231
);

plugins/info-cards/src/components/InfoRow.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import type { InfoRowI } from "../typings";
2-
import { Box } from "@chakra-ui/react";
2+
import { Box, theme } from "@chakra-ui/react";
33
import InfoCard from "./InfoCard";
44

55
interface InfoRowProps {
66
infoRow: InfoRowI;
77
}
88
export default function InfoRow({ infoRow }: InfoRowProps): JSX.Element {
99
return (
10-
<Box display={"flex"} gap={4} width={"full"} justifyContent={"center"}>
10+
<Box
11+
display={"flex"}
12+
gap={4}
13+
width={"full"}
14+
justifyContent={"center"}
15+
style={
16+
infoRow.cards.length < 1
17+
? { height: "1px", backgroundColor: theme.colors.gray[100] }
18+
: {}
19+
}
20+
>
1121
{infoRow.cards.map((card) => (
1222
<InfoCard key={card.id} card={card} />
1323
))}

plugins/info-cards/src/components/PluginRoot.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,21 @@ import type { InfoRowI } from "../typings";
99
import LandingPage from "./LandingPage";
1010
import LayoutBuilder from "./LayoutBuilder";
1111

12-
import { usePluginConfig } from "../hooks";
12+
import { useEntityDescriptor } from "../hooks";
1313

1414
export default function PluginRoot(): JSX.Element {
1515
const [isEditorPage, setIsEditorPage] = useState(false);
1616
const [infoRows, setInfoRows] = useState<InfoRowI[]>([]);
1717

1818
const {
1919
isLoading: configIsLoading,
20-
pluginConfig,
21-
savePluginConfig,
22-
} = usePluginConfig();
20+
isFetching: configIsFetching,
21+
isMutating: configIsMutating,
22+
entity: pluginConfig,
23+
updateEntity: savePluginConfig,
24+
} = useEntityDescriptor({
25+
entityTag: "info-cards-plugin-config",
26+
});
2327

2428
const toast = useToast();
2529

@@ -37,16 +41,21 @@ export default function PluginRoot(): JSX.Element {
3741
return Boolean(isModified);
3842
}, [infoRows, pluginConfig]);
3943

44+
const toggleEditor = useCallback(() => {
45+
setInfoRows(pluginConfig?.info?.["x-cortex-definition"]?.infoRows || []);
46+
setIsEditorPage((prev) => !prev);
47+
}, [pluginConfig, setInfoRows, setIsEditorPage]);
48+
4049
const handleSubmit = useCallback(() => {
4150
const doSave = async (): Promise<void> => {
4251
try {
43-
await savePluginConfig({
52+
savePluginConfig({
4453
...pluginConfig,
4554
info: {
4655
...pluginConfig?.info,
4756
"x-cortex-definition": {
4857
...(pluginConfig?.info?.["x-cortex-definition"] || {}),
49-
infoRows,
58+
infoRows: [...infoRows],
5059
},
5160
},
5261
});
@@ -71,16 +80,12 @@ export default function PluginRoot(): JSX.Element {
7180
};
7281

7382
void doSave();
74-
}, [infoRows, pluginConfig, savePluginConfig, toast]);
83+
}, [infoRows, pluginConfig, savePluginConfig, toast, toggleEditor]);
7584

76-
if (configIsLoading) {
85+
if (configIsLoading || configIsFetching || configIsMutating) {
7786
return <Loader />;
7887
}
7988

80-
const toggleEditor = (): void => {
81-
setIsEditorPage((prev) => !prev);
82-
};
83-
8489
return (
8590
<>
8691
{isModified && (

plugins/info-cards/src/hooks.tsx

Lines changed: 77 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,98 @@
1-
import { useCallback, useEffect, useState } from "react";
2-
import YAML from "yaml";
1+
import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
2+
3+
import type { UseMutationResult } from "@tanstack/react-query";
34

45
import { usePluginContext } from "@cortexapps/plugin-core/components";
56

6-
export interface UsePluginConfigReturn {
7+
export interface UseEntityDescriptorProps {
8+
entityTag: string;
9+
mutationMethod?: "PATCH" | "POST";
10+
onMutateSuccess?: (data: any, variables: any, context?: any) => void;
11+
onMutateError?: (error: Error, variables: any, context?: any) => void;
12+
onMutateSettled?: (
13+
data: any,
14+
error: Error,
15+
variables: any,
16+
context?: any
17+
) => void;
18+
onMutate?: (variables: any) => void;
19+
}
20+
21+
export interface UseEntityDescriptorReturn {
22+
entity: any;
723
isLoading: boolean;
8-
pluginConfig: any | null;
9-
savePluginConfig: (config: any) => Promise<void>;
10-
refreshPluginConfig: () => void;
24+
isFetching: boolean;
25+
error: unknown;
26+
updateEntity: UseMutationResult<any, Error, any>["mutate"];
27+
isMutating: boolean;
1128
}
1229

13-
export const usePluginConfig = (): UsePluginConfigReturn => {
30+
export const useEntityDescriptor = ({
31+
entityTag,
32+
mutationMethod = "POST",
33+
onMutateSuccess = () => {},
34+
onMutateError = () => {},
35+
onMutateSettled = () => {},
36+
onMutate = () => {},
37+
}: UseEntityDescriptorProps): UseEntityDescriptorReturn => {
1438
const { apiBaseUrl } = usePluginContext();
1539

16-
const [refreshCounter, setRefreshCounter] = useState(0);
17-
const [isLoading, setIsLoading] = useState(true);
18-
const [pluginConfig, setPluginConfig] = useState<any | null>(null);
19-
20-
useEffect(() => {
21-
const fetchPluginConfig = async (): Promise<void> => {
22-
setIsLoading(true);
23-
setPluginConfig(null);
24-
try {
25-
const response = await fetch(
26-
`${apiBaseUrl}/catalog/info-cards-plugin-config/openapi`
27-
);
28-
const config = await response.json();
29-
setPluginConfig(config);
30-
} catch (error) {
31-
console.error(error);
32-
} finally {
33-
setIsLoading(false);
34-
}
35-
};
36-
void fetchPluginConfig();
37-
}, [apiBaseUrl, refreshCounter]);
38-
39-
const savePluginConfig = useCallback(
40-
async (config: any) => {
41-
let existingConfig: any;
40+
const queryClient = useQueryClient();
4241

43-
// Fetch existing config if it exists
44-
try {
45-
const r = await fetch(
46-
`${apiBaseUrl}/catalog/info-cards-plugin-config/openapi`
47-
);
48-
if (!r.ok) {
49-
throw new Error("Failed to fetch existing config");
50-
}
51-
existingConfig = await r.json();
52-
} catch (error) {}
42+
const query = useQuery({
43+
queryKey: ["entityDescriptor", entityTag],
44+
queryFn: async () => {
45+
const response = await fetch(
46+
`${apiBaseUrl}/catalog/${entityTag}/openapi`
47+
);
48+
return await response.json();
49+
},
50+
enabled: !!apiBaseUrl,
51+
retry: false,
52+
});
5353

54-
// Validate the passed in config
55-
if (!config.info?.["x-cortex-definition"]?.infoRows) {
56-
// this should never happen since the plugin should always pass in a valid config
57-
console.error("Invalid config", config);
58-
throw new Error("Invalid config");
54+
const mutation = useMutation({
55+
mutationFn: async (data: any) => {
56+
// throw if the data is not an object or data.info is not an object
57+
if (typeof data !== "object" || typeof data.info !== "object") {
58+
throw new Error("Invalid data format");
5959
}
60-
61-
config.info["x-cortex-tag"] = "info-cards-plugin-config";
62-
config.info.title = "Info Cards Plugin Configuration";
63-
config.openapi = "3.0.1";
64-
65-
// Preserve the existing x-cortex-type if it exists
66-
config.info["x-cortex-type"] =
67-
existingConfig?.info?.["x-cortex-type"] || "plugin-configuration";
68-
69-
// See if the entity type exists, if not create it
70-
try {
71-
const r = await fetch(
72-
`${apiBaseUrl}/catalog/definitions/${
73-
config.info["x-cortex-type"] as string
74-
}`
75-
);
76-
if (!r.ok) {
77-
throw new Error("Failed to fetch existing entity type");
78-
}
79-
} catch (error) {
80-
// Create the entity type
81-
const entityTypeBody = {
82-
iconTag: "bucket",
83-
name: "Plugin Configuration",
84-
schema: { properties: {}, required: [] },
85-
type: config.info["x-cortex-type"],
86-
};
87-
const entityTypeResponse = await fetch(
88-
`${apiBaseUrl}/catalog/definitions`,
89-
{
90-
method: "POST",
91-
headers: {
92-
"Content-Type": "application/json",
93-
},
94-
body: JSON.stringify(entityTypeBody),
95-
}
96-
);
97-
if (!entityTypeResponse.ok) {
98-
throw new Error("Failed to create entity type");
99-
}
60+
// make sure basic info is set
61+
data.openapi = "3.0.1";
62+
// don't allow changing the tag
63+
data.info["x-cortex-tag"] = entityTag;
64+
// set a title if it's not set
65+
if (!data.info.title) {
66+
data.info.title = entityTag
67+
.replace(/-/g, " ")
68+
.replace(/\b\w/g, (l) => l.toUpperCase());
10069
}
101-
102-
// Save the new config
103-
await fetch(`${apiBaseUrl}/open-api`, {
104-
method: "POST",
70+
const response = await fetch(`${apiBaseUrl}/open-api`, {
71+
method: mutationMethod,
10572
headers: {
10673
"Content-Type": "application/openapi;charset=utf-8",
10774
},
108-
body: YAML.stringify(config),
75+
body: JSON.stringify(data),
10976
});
110-
111-
setRefreshCounter((prev) => prev + 1);
77+
return await response.json();
11278
},
113-
[apiBaseUrl]
114-
);
115-
116-
const refreshPluginConfig = useCallback(() => {
117-
setRefreshCounter((prev) => prev + 1);
118-
}, []);
79+
onMutate,
80+
onError: onMutateError,
81+
onSettled: onMutateSettled,
82+
onSuccess: (data, variables, context) => {
83+
void queryClient.invalidateQueries({
84+
queryKey: ["entityDescriptor", entityTag],
85+
});
86+
onMutateSuccess(data, variables, context);
87+
},
88+
});
11989

12090
return {
121-
isLoading,
122-
pluginConfig,
123-
savePluginConfig,
124-
refreshPluginConfig,
91+
entity: query.data,
92+
isLoading: query.isLoading,
93+
isFetching: query.isFetching,
94+
error: query.error,
95+
updateEntity: mutation.mutate,
96+
isMutating: mutation.isPending,
12597
};
12698
};

0 commit comments

Comments
 (0)