Skip to content

Commit ef069f8

Browse files
committed
feat: refactored page content component
1 parent c1882af commit ef069f8

File tree

8 files changed

+208
-113
lines changed

8 files changed

+208
-113
lines changed

plugins/confluence-plugin/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"version": "0.1.0",
44
"license": "MIT",
55
"dependencies": {
6+
"@chakra-ui/react": "^2.10.2",
67
"@cortexapps/plugin-core": "^2.0.0",
8+
"@emotion/react": "^11.14.0",
9+
"@emotion/styled": "^11.14.0",
10+
"framer-motion": "^11.15.0",
711
"react": "^18.2.0",
812
"react-dom": "^18.2.0"
913
},
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export const fetchConfluencePageContent = async (
2+
baseConfluenceUrl: string,
3+
pageId: string | number
4+
): Promise<any> => {
5+
const jiraURL = `${baseConfluenceUrl}/wiki/rest/api/content/${pageId}?expand=body.view`;
6+
let contentResult;
7+
try {
8+
contentResult = await fetch(jiraURL);
9+
} catch (error: any) {
10+
throw new Error(
11+
`Network error while fetching Confluence page with ID ${pageId}: ${
12+
(error as Error).message
13+
}`
14+
);
15+
}
16+
if (!contentResult.ok) {
17+
let errorStr = "";
18+
try {
19+
if (
20+
contentResult.headers.get("content-type")?.includes("application/json")
21+
) {
22+
const contentJSON: { message?: string } = await contentResult.json();
23+
errorStr = `Failed to fetch Confluence page with ID ${pageId}: ${
24+
contentJSON.message ?? JSON.stringify(contentJSON)
25+
}`;
26+
} else {
27+
errorStr = await contentResult.text();
28+
}
29+
} catch {
30+
errorStr = contentResult.statusText || "Failed to fetch Confluence page";
31+
}
32+
throw new Error(errorStr);
33+
}
34+
return contentResult.json();
35+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export const fetchConfluencePluginConfig = async (
2+
apiBaseUrl: string
3+
): Promise<string> => {
4+
try {
5+
const response = await fetch(
6+
`${apiBaseUrl}/catalog/confluence-plugin-config/openapi`
7+
);
8+
if (!response.ok) {
9+
throw new Error(`HTTP error! status: ${response.status}`);
10+
}
11+
const data = await response.json();
12+
return data.info["x-cortex-definition"]["confluence-url"];
13+
} catch (error) {
14+
console.error("Error fetching Confluence plugin config:", error);
15+
throw error;
16+
}
17+
};

plugins/confluence-plugin/src/components/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import { PluginProvider } from "@cortexapps/plugin-core/components";
33
import "../baseStyles.css";
44
import ErrorBoundary from "./ErrorBoundary";
55
import PageContent from "./PageContent";
6+
import { ChakraProvider } from "@chakra-ui/react";
67

78
const App: React.FC = () => {
89
return (
910
<ErrorBoundary>
1011
<PluginProvider>
11-
<PageContent />
12+
<ChakraProvider toastOptions={{ defaultOptions: { position: "top" } }}>
13+
<PageContent />
14+
</ChakraProvider>
1215
</PluginProvider>
1316
</ErrorBoundary>
1417
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Box, Spinner, Text } from "@chakra-ui/react";
2+
3+
export default function Loading(): JSX.Element {
4+
return (
5+
<Box
6+
w={"full"}
7+
minH={400}
8+
display={"flex"}
9+
justifyContent={"center"}
10+
alignItems={"center"}
11+
flexDirection={"column"}
12+
gap={6}
13+
>
14+
<Spinner color="purple" size="xl" />
15+
<Text size="md">Loading...</Text>
16+
</Box>
17+
);
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Box, Text } from "@chakra-ui/react";
2+
import type { ReactNode } from "react";
3+
4+
export default function Notice({
5+
children,
6+
}: {
7+
children: ReactNode;
8+
}): JSX.Element {
9+
return (
10+
<Box bg="gray.200" p={3} borderRadius={4}>
11+
<Text m={0}>{children}</Text>
12+
</Box>
13+
);
14+
}

plugins/confluence-plugin/src/components/PageContent.tsx

Lines changed: 75 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -2,102 +2,81 @@ import type React from "react";
22
import { useState, useEffect, useCallback } from "react";
33
import { isEmpty, isNil } from "lodash";
44
import "../baseStyles.css";
5-
import {
6-
Box,
7-
Text,
8-
usePluginContext,
9-
} from "@cortexapps/plugin-core/components";
5+
import { usePluginContext } from "@cortexapps/plugin-core/components";
106
import { getEntityYaml } from "../api/Cortex";
117
import { getConfluenceDetailsFromEntity } from "../lib/parseEntity";
12-
8+
import { Heading, Box } from "@chakra-ui/react";
139
import Instructions from "./Instructions";
10+
import Loading from "./Loading";
11+
import Notice from "./Notice";
12+
import PageSelector from "./PageSelector";
13+
import { fetchConfluencePageContent } from "../api/fetchConfluencePageContent";
14+
import { fetchConfluencePluginConfig } from "../api/fetchConfluencePluginConfig";
1415

1516
const PageContent: React.FC = () => {
1617
const [entityPages, setEntityPages] = useState<EntityPageI[]>([]);
1718
const [pageContent, setPageContent] = useState<string | undefined>();
1819
const [pageTitle, setPageTitle] = useState<string | undefined>();
19-
const [entityPage, setEntityPage] = useState<any | string>();
20+
const [entityPage, setEntityPage] = useState<string | number | undefined>();
2021
const [baseConfluenceUrl, setBaseConfluenceUrl] = useState<string>("");
2122
const [errorStr, setErrorStr] = useState<string>("");
2223

2324
const context = usePluginContext();
2425

2526
const fetchPageContent = useCallback(
2627
async (pages: EntityPageI[], pageId: string | number): Promise<void> => {
27-
if (pages.length === 0) {
28-
return;
29-
}
28+
if (pages.length === 0) return;
3029
setEntityPage(pageId);
31-
const jiraURL = `${baseConfluenceUrl}/wiki/rest/api/content/${pageId}?expand=body.view`;
32-
setErrorStr("loading");
33-
const contentResult = await fetch(jiraURL);
34-
if (!contentResult.ok) {
35-
let newErrorStr = "";
36-
// if the contentResult contains valid JSON, we can use it to display an error message
37-
try {
38-
if (
39-
contentResult.headers
40-
.get("content-type")
41-
?.includes("application/json")
42-
) {
43-
const contentJSON = await contentResult.json();
44-
const msg: string =
45-
contentJSON.message || JSON.stringify(contentJSON);
46-
newErrorStr = `Failed to fetch Confluence page with ID ${pageId}: ${msg}`;
47-
} else {
48-
// just get the text if it's not JSON
49-
const contentText = await contentResult.text();
50-
newErrorStr = contentText;
51-
}
52-
} catch (e) {
53-
// if we can't parse the content, just use the status text
54-
newErrorStr =
55-
contentResult.statusText || "Failed to fetch Confluence page";
56-
}
57-
setErrorStr(newErrorStr);
58-
return;
30+
setErrorStr("loading-content");
31+
try {
32+
const contentJSON = await fetchConfluencePageContent(
33+
baseConfluenceUrl,
34+
pageId
35+
);
36+
setPageContent(contentJSON.body.view.value);
37+
setPageTitle(contentJSON.title);
38+
setErrorStr("");
39+
} catch (error) {
40+
setErrorStr(error.message);
5941
}
60-
const contentJSON = await contentResult.json();
61-
setPageContent(contentJSON.body.view.value);
62-
setPageTitle(contentJSON.title);
63-
setErrorStr("");
6442
},
6543
[baseConfluenceUrl]
6644
);
6745

6846
useEffect(() => {
69-
if (!context?.apiBaseUrl) {
70-
return;
71-
}
72-
const getConfluencePluginConfig = async (): Promise<void> => {
47+
if (!context?.apiBaseUrl) return;
48+
const getConfig = async (): Promise<void> => {
7349
setErrorStr("loading");
74-
if (!context?.apiBaseUrl) {
75-
return;
76-
}
77-
let newConfluenceUrl = "";
7850
try {
79-
const response = await fetch(
80-
`${context?.apiBaseUrl}/catalog/confluence-plugin-config/openapi`
51+
const newConfluenceUrl = await fetchConfluencePluginConfig(
52+
context.apiBaseUrl
8153
);
82-
const data = await response.json();
83-
newConfluenceUrl = data.info["x-cortex-definition"]["confluence-url"];
84-
} catch (e) {}
85-
setBaseConfluenceUrl(newConfluenceUrl);
86-
if (!newConfluenceUrl) {
54+
setBaseConfluenceUrl(newConfluenceUrl);
55+
setErrorStr(!newConfluenceUrl ? "instructions" : "");
56+
} catch {
8757
setErrorStr("instructions");
8858
}
8959
};
90-
void getConfluencePluginConfig();
60+
void getConfig();
9161
}, [context?.apiBaseUrl]);
9262

9363
useEffect(() => {
94-
if (!context.apiBaseUrl || !context.entity?.tag || !baseConfluenceUrl) {
64+
if (!context.entity?.tag) {
65+
setErrorStr(
66+
"This plugin is intended to be used within the entities. " +
67+
"Go to an entity, then under Plugins, select Confluence to view the Confluence page(s)."
68+
);
69+
}
70+
71+
if (!context.apiBaseUrl || !baseConfluenceUrl) {
9572
return;
9673
}
97-
const fetchEntityYaml = async (): Promise<void> => {
74+
75+
const fetchEntityYamlData = async (): Promise<void> => {
9876
const entityTag = context.entity?.tag;
9977
if (!isNil(entityTag)) {
10078
try {
79+
setErrorStr("loading");
10180
const yaml = await getEntityYaml(context.apiBaseUrl, entityTag);
10281
const fetchedEntityPages = isEmpty(yaml)
10382
? []
@@ -107,79 +86,63 @@ const PageContent: React.FC = () => {
10786
return;
10887
}
10988
setEntityPages(fetchedEntityPages);
110-
} catch (e) {
111-
// This will still result in a "We could not find any Confluence page" error in the UI, but may as well trap in console as well
112-
const msg: string = e.message || e.toString();
113-
setErrorStr(`Error retrieving Confluence page: ${msg}`);
114-
console.error("Error retrieving Confluence page: ", e);
89+
} catch (error) {
90+
setErrorStr(
91+
`Error retrieving Confluence page: ${(error as Error).message}`
92+
);
93+
console.error("Error retrieving Confluence page: ", error);
11594
}
11695
}
11796
};
118-
void fetchEntityYaml();
97+
void fetchEntityYamlData();
11998
}, [context.apiBaseUrl, context.entity?.tag, baseConfluenceUrl]);
12099

121100
useEffect(() => {
122101
const setFirstPageContent = async (): Promise<void> => {
123-
if (entityPages.length === 0) {
124-
return;
125-
}
102+
if (entityPages.length === 0) return;
126103
await fetchPageContent(entityPages, entityPages[0].id);
127104
};
128105
void setFirstPageContent();
129106
}, [baseConfluenceUrl, entityPages, fetchPageContent]);
130107

131-
if (errorStr === "loading") {
132-
return <div>Loading...</div>;
133-
} else if (errorStr === "instructions") {
134-
return <Instructions />;
135-
} else if (errorStr) {
136-
return (
137-
<Box backgroundColor="light" padding={3} borderRadius={2}>
138-
<Text>{errorStr}</Text>
139-
</Box>
140-
);
141-
}
108+
if (errorStr === "loading") return <Loading />;
109+
if (errorStr === "instructions") return <Instructions />;
110+
if (errorStr && errorStr !== "loading-content")
111+
return <Notice>{errorStr}</Notice>;
142112

143113
if (isNil(entityPage)) {
144114
return (
145-
<Box backgroundColor="light" padding={3} borderRadius={2}>
146-
<Text>
147-
We could not find any Confluence page associated with this entity.
148-
</Text>
149-
</Box>
115+
<Notice>
116+
We could not find any Confluence page associated with this entity.
117+
</Notice>
150118
);
151119
}
152120

153121
return (
154-
<div>
122+
<Box w="full" minH={600}>
155123
{entityPages.length > 1 && (
156-
<div
157-
style={{
158-
marginBottom: "1rem",
159-
display: "flex",
160-
justifyContent: "flex-end",
161-
width: "100%",
162-
}}
163-
>
164-
<select
165-
value={entityPage}
166-
onChange={(e) => {
167-
void fetchPageContent(entityPages, e.target.value);
168-
}}
169-
>
170-
{entityPages.map((page) => (
171-
<option key={page.id} value={page.id}>
172-
{page.title && page.title.length > 0
173-
? page.title
174-
: `Page ID: ${page.id}`}
175-
</option>
176-
))}
177-
</select>
178-
</div>
124+
<PageSelector
125+
currentPageId={entityPage}
126+
onChangeHandler={fetchPageContent}
127+
pages={entityPages}
128+
disabled={errorStr === "loading-content"}
129+
/>
130+
)}
131+
{errorStr === "loading-content" ? (
132+
<Loading />
133+
) : (
134+
<Box w="full">
135+
<Heading
136+
as="h1"
137+
dangerouslySetInnerHTML={{ __html: pageTitle as string }}
138+
/>
139+
<Box
140+
w="full"
141+
dangerouslySetInnerHTML={{ __html: pageContent as string }}
142+
/>
143+
</Box>
179144
)}
180-
<h1 dangerouslySetInnerHTML={{ __html: pageTitle as string }}></h1>
181-
<p dangerouslySetInnerHTML={{ __html: pageContent as string }}></p>
182-
</div>
145+
</Box>
183146
);
184147
};
185148

0 commit comments

Comments
 (0)