Skip to content

Commit 704372b

Browse files
a-asaadjhunzik
andcommitted
feat(ui): add a modal for deploying flow diagrams
Co-authored-by: Josh Hunziker <[email protected]> Co-authored-by: Abdullah Asaad <[email protected]>
1 parent 48075e4 commit 704372b

File tree

13 files changed

+258
-45
lines changed

13 files changed

+258
-45
lines changed

ui/.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
VITE_FLOW_TRANSLATOR_BASE_URL=http://localhost:8080
22
VITE_KEIP_ASSISTANT_DOCS_URL=https://github.com/codice/keip-canvas/blob/main/assistant/README.md
3-
VITE_KEIP_ASSISTANT_OLLAMA_URL=http://localhost:11434
3+
VITE_KEIP_ASSISTANT_OLLAMA_URL=http://localhost:11434
4+
VITE_KEIP_K8S_CLUSTER_URL=http://localhost:7080
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { K8S_CLUSTER_URL } from "../../../singletons/externalEndpoints"
2+
import fetchWithTimeout from "../../../utils/fetch/fetchWithTimeout"
3+
4+
interface Route {
5+
name: string
6+
namespace: string
7+
xml: string
8+
}
9+
10+
interface Resource {
11+
name: string
12+
status: "created" | "deleted" | "updated" | "recreated"
13+
}
14+
15+
interface RouteDeployRequest {
16+
routes: Route[]
17+
}
18+
19+
type RouteDeployResponse = Resource[]
20+
21+
class KeipClient {
22+
public serverBaseUrl = K8S_CLUSTER_URL
23+
24+
public ping(): Promise<boolean> {
25+
const status = fetchWithTimeout(`${this.serverBaseUrl}/status`, {
26+
timeout: 5000,
27+
})
28+
.then((res) => res.ok)
29+
.catch(() => false)
30+
return status
31+
}
32+
33+
public async deploy(
34+
request: RouteDeployRequest
35+
): Promise<RouteDeployResponse> {
36+
const response = await fetchWithTimeout(`${this.serverBaseUrl}/route`, {
37+
method: "PUT",
38+
headers: {
39+
"Content-Type": "application/json",
40+
},
41+
body: JSON.stringify(request),
42+
timeout: 10000,
43+
})
44+
45+
if (!response.ok) {
46+
throw new Error(`Failed to deploy route: ${response.status}`)
47+
}
48+
49+
return (await response.json()) as RouteDeployResponse
50+
}
51+
}
52+
53+
export const keipClient = new KeipClient()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useEffect, useState } from "react"
2+
import { keipClient } from "./keipClient"
3+
4+
const logKeipClientStatus = (available: boolean) => {
5+
if (available) {
6+
console.log(`A KEIP Controller is available at ${keipClient.serverBaseUrl}`)
7+
} else {
8+
console.log(
9+
`Could not connect to a KEIP Controller at ${keipClient.serverBaseUrl}`
10+
)
11+
}
12+
}
13+
14+
const useKeipClientStatus = () => {
15+
const [isAvailable, setIsAvailable] = useState(false)
16+
17+
useEffect(() => {
18+
void (async () => {
19+
const status = await keipClient.ping()
20+
logKeipClientStatus(status)
21+
setIsAvailable(status)
22+
})()
23+
}, [])
24+
25+
return isAvailable
26+
}
27+
28+
export default useKeipClientStatus
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { EipFlow } from "../../../api/generated/eipFlow"
2+
3+
// highlight.js theme
4+
import "highlight.js/styles/intellij-light.css"
5+
import { FLOW_TRANSLATOR_BASE_URL } from "../../../singletons/externalEndpoints"
6+
import fetchWithTimeout from "../../../utils/fetch/fetchWithTimeout"
7+
8+
interface FlowTranslationResponse {
9+
data?: string
10+
error?: {
11+
message: string
12+
type: string
13+
details: object[]
14+
}
15+
}
16+
17+
// TODO: Add client-side caching (might make sense to use a data fetching library)
18+
export const fetchXmlTranslation = async (
19+
flow: EipFlow,
20+
abortCtrl: AbortController
21+
) => {
22+
const queryStr = new URLSearchParams({ prettyPrint: "true" }).toString()
23+
const response = await fetchWithTimeout(
24+
`${FLOW_TRANSLATOR_BASE_URL}/translation/toSpringXml?` + queryStr,
25+
{
26+
method: "POST",
27+
body: JSON.stringify(flow),
28+
headers: {
29+
"Content-Type": "application/json",
30+
},
31+
timeout: 20000,
32+
abortCtrl,
33+
}
34+
)
35+
36+
const { data, error } = (await response.json()) as FlowTranslationResponse
37+
38+
if (!response.ok) {
39+
console.error("Failed to convert diagram to XML:", error)
40+
throw new Error(JSON.stringify(error))
41+
}
42+
43+
return data!
44+
}

ui/src/components/toolbar/xml/translatorStatusHook.ts renamed to ui/src/components/endpoints/translation/translatorStatusHook.ts

File renamed without changes.

ui/src/components/options-menu/OptionsMenu.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { OverflowMenu, OverflowMenuItem } from "@carbon/react"
22
import { Menu } from "@carbon/react/icons"
33
import { useState } from "react"
4+
import useKeipClientStatus from "../endpoints/deploy/keipClientStatusHook"
5+
import useTranslationServerStatus from "../endpoints/translation/translatorStatusHook"
46
import ExportPng from "./ExportPng"
57
import SaveDiagram from "./SaveDiagram"
8+
import { DeployRouteModal } from "./modals/DeployRouteModal"
69
import { ImportFlowModal } from "./modals/ImportFlowModal"
710

811
const OptionsMenu = () => {
912
const [importFlowModalOpen, setImportFlowModalOpen] = useState(false)
13+
const [deployModalOpen, setDeployModalOpen] = useState(false)
14+
const isKeipWebhookAvailable = useKeipClientStatus()
15+
const isTranslationServerAvailable = useTranslationServerStatus()
1016

1117
return (
1218
<>
@@ -26,13 +32,21 @@ const OptionsMenu = () => {
2632
itemText="Import Flow JSON"
2733
onClick={() => setImportFlowModalOpen(true)}
2834
/>
35+
36+
{/* Route deployment requires both the flow translator and Keip controller endpoints to be available */}
37+
<OverflowMenuItem
38+
itemText="Deploy Route"
39+
onClick={() => setDeployModalOpen(true)}
40+
disabled={!isKeipWebhookAvailable || !isTranslationServerAvailable}
41+
/>
2942
</OverflowMenu>
3043

3144
{/* Modals */}
3245
<ImportFlowModal
3346
open={importFlowModalOpen}
3447
setOpen={setImportFlowModalOpen}
3548
/>
49+
<DeployRouteModal open={deployModalOpen} setOpen={setDeployModalOpen} />
3650
</>
3751
)
3852
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Modal, Stack, TextInput } from "@carbon/react"
2+
import { InlineLoadingStatus } from "@carbon/react/lib/components/InlineLoading/InlineLoading"
3+
import { useState } from "react"
4+
import { createPortal } from "react-dom"
5+
import { getEipFlow } from "../../../singletons/store/storeViews"
6+
import { keipClient } from "../../endpoints/deploy/keipClient"
7+
import { fetchXmlTranslation } from "../../endpoints/translation/translateFlowToXml"
8+
9+
interface DeployRouteModalProps {
10+
open: boolean
11+
setOpen: (open: boolean) => void
12+
}
13+
14+
const getLoadingDescription = (status: InlineLoadingStatus) => {
15+
switch (status) {
16+
case "active":
17+
return "Deploying Route"
18+
case "error":
19+
return "Deploy Failed"
20+
case "finished":
21+
return "Deployed"
22+
case "inactive":
23+
return ""
24+
}
25+
}
26+
27+
// TODO: Display status for deployed routes once Keip controller endpoint adds support for status checks.
28+
// TODO: Store deployed route name and namespace in the AppStore to survive refreshes.
29+
export const DeployRouteModal = ({ open, setOpen }: DeployRouteModalProps) => {
30+
const [loadingStatus, setLoadingStatus] =
31+
useState<InlineLoadingStatus>("inactive")
32+
33+
const [name, setName] = useState("")
34+
const [namespace, setNamespace] = useState("default")
35+
36+
const onClose = () => {
37+
setOpen(false)
38+
loadingStatus !== "active" && setLoadingStatus("inactive")
39+
}
40+
41+
const handleDeploy = () => {
42+
setLoadingStatus("active")
43+
deployDiagram()
44+
.then(() => setLoadingStatus("finished"))
45+
.catch((err) => {
46+
console.error(err)
47+
setLoadingStatus("error")
48+
})
49+
}
50+
51+
const deployDiagram = async () => {
52+
const flow = getEipFlow()
53+
54+
if (flow.nodes?.length === 0) {
55+
throw new Error("Failed to deploy - diagram is empty")
56+
}
57+
58+
const xml = await fetchXmlTranslation(flow, new AbortController())
59+
60+
if (!xml) {
61+
throw new Error("Failed to deploy - translated XML is empty")
62+
}
63+
64+
const request = {
65+
routes: [
66+
{
67+
name,
68+
namespace,
69+
xml,
70+
},
71+
],
72+
}
73+
74+
await keipClient.deploy(request)
75+
}
76+
77+
return createPortal(
78+
<Modal
79+
loadingDescription={getLoadingDescription(loadingStatus)}
80+
loadingStatus={loadingStatus}
81+
modalHeading="Deploy Routes"
82+
onRequestClose={onClose}
83+
onRequestSubmit={handleDeploy}
84+
open={open}
85+
primaryButtonText="Deploy"
86+
secondaryButtonText="Cancel"
87+
>
88+
<Stack gap={6}>
89+
<TextInput
90+
id="name-input"
91+
labelText="Name"
92+
placeholder="Route name"
93+
value={name}
94+
onChange={(e) => setName(e.target.value)}
95+
/>
96+
<TextInput
97+
id="namespace-input"
98+
labelText="Namespace"
99+
value={namespace}
100+
onChange={(e) => setNamespace(e.target.value)}
101+
/>
102+
</Stack>
103+
</Modal>,
104+
document.body
105+
)
106+
}

ui/src/components/toolbar/ToolbarMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { KEIP_ASSISTANT_DOCS_URL } from "../../singletons/externalEndpoints"
55
import AssistantChatPanel from "./assistant/AssistantChatPanel"
66
import { useLlmServerStatus } from "./assistant/llmStatusHook"
77
import XmlPanel from "./xml/XmlPanel"
8-
import useTranslationServerStatus from "./xml/translatorStatusHook"
8+
import useTranslationServerStatus from "../endpoints/translation/translatorStatusHook"
99

1010
interface PanelButtonProps {
1111
name: string

ui/src/components/toolbar/xml/XmlPanel.tsx

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,16 @@ import hljs from "highlight.js/lib/core"
33
import xml from "highlight.js/lib/languages/xml"
44
import { useEffect, useState } from "react"
55
import Editor from "react-simple-code-editor"
6-
import { EipFlow } from "../../../api/generated/eipFlow"
76
import { useEipFlow } from "../../../singletons/store/diagramToEipFlow"
87

98
// highlight.js theme
109
import "highlight.js/styles/intellij-light.css"
11-
import { FLOW_TRANSLATOR_BASE_URL } from "../../../singletons/externalEndpoints"
12-
import fetchWithTimeout from "../../../utils/fetch/fetchWithTimeout"
10+
import { fetchXmlTranslation } from "../../endpoints/translation/translateFlowToXml"
1311

1412
const UNMOUNT_SIGNAL = "unmount"
1513

1614
hljs.registerLanguage("xml", xml)
1715

18-
interface FlowTranslationResponse {
19-
data?: string
20-
error?: {
21-
message: string
22-
type: string
23-
details: object[]
24-
}
25-
}
26-
2716
type InlineLoadingProps = React.ComponentProps<typeof InlineLoading>
2817

2918
const getLoadingStatus = (
@@ -38,35 +27,6 @@ const getLoadingStatus = (
3827
: { status: "finished", description: "synced" }
3928
}
4029

41-
// TODO: Add client-side caching (might make sense to use a data fetching library)
42-
const fetchXmlTranslation = async (
43-
flow: EipFlow,
44-
abortCtrl: AbortController
45-
) => {
46-
const queryStr = new URLSearchParams({ prettyPrint: "true" }).toString()
47-
const response = await fetchWithTimeout(
48-
`${FLOW_TRANSLATOR_BASE_URL}/translation/toSpringXml?` + queryStr,
49-
{
50-
method: "POST",
51-
body: JSON.stringify(flow),
52-
headers: {
53-
"Content-Type": "application/json",
54-
},
55-
timeout: 20000,
56-
abortCtrl,
57-
}
58-
)
59-
60-
const { data, error } = (await response.json()) as FlowTranslationResponse
61-
62-
if (!response.ok) {
63-
console.error("Failed to convert diagram to XML:", error)
64-
throw new Error(JSON.stringify(error))
65-
}
66-
67-
return data!
68-
}
69-
7030
const XmlPanel = () => {
7131
const [content, setContent] = useState("")
7232
const [isLoading, setLoading] = useState(false)

ui/src/singletons/externalEndpoints.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export const KEIP_ASSISTANT_DOCS_URL = import.meta.env
66

77
export const KEIP_ASSISTANT_OLLAMA_URL = import.meta.env
88
.VITE_KEIP_ASSISTANT_OLLAMA_URL
9+
10+
export const K8S_CLUSTER_URL = import.meta.env.VITE_KEIP_K8S_CLUSTER_URL

0 commit comments

Comments
 (0)