Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ui/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
VITE_FLOW_TRANSLATOR_BASE_URL=http://localhost:8080
VITE_KEIP_ASSISTANT_DOCS_URL=https://github.com/codice/keip-canvas/blob/main/assistant/README.md
VITE_KEIP_ASSISTANT_OLLAMA_URL=http://localhost:11434
VITE_KEIP_ASSISTANT_OLLAMA_URL=http://localhost:11434
VITE_KEIP_K8S_CLUSTER_URL=http://localhost:7080
8 changes: 4 additions & 4 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"sass": "^1.89.2",
"typescript": "~5.6.3",
"typescript-eslint": "^8.34.0",
"vite": "^6.3.5",
"vite": "^6.3.6",
"vitest": "^3.2.3"
},
"overrides": {
Expand Down
53 changes: 53 additions & 0 deletions ui/src/components/endpoints/deploy/keipClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { K8S_CLUSTER_URL } from "../../../singletons/externalEndpoints"
import fetchWithTimeout from "../../../utils/fetch/fetchWithTimeout"

interface Route {
name: string
namespace: string
xml: string
}

interface Resource {
name: string
status: "created" | "deleted" | "updated" | "recreated"
}

interface RouteDeployRequest {
routes: Route[]
}

type RouteDeployResponse = Resource[]

class KeipClient {
public serverBaseUrl = K8S_CLUSTER_URL

public ping(): Promise<boolean> {
const status = fetchWithTimeout(`${this.serverBaseUrl}/status`, {
timeout: 5000,
})
.then((res) => res.ok)
.catch(() => false)
return status
}

public async deploy(
request: RouteDeployRequest
): Promise<RouteDeployResponse> {
const response = await fetchWithTimeout(`${this.serverBaseUrl}/route`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
timeout: 10000,
})

if (!response.ok) {
throw new Error(`Failed to deploy route: ${response.status}`)
}

return (await response.json()) as RouteDeployResponse
}
}

export const keipClient = new KeipClient()
28 changes: 28 additions & 0 deletions ui/src/components/endpoints/deploy/keipClientStatusHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from "react"
import { keipClient } from "./keipClient"

const logKeipClientStatus = (available: boolean) => {
if (available) {
console.log(`A KEIP Controller is available at ${keipClient.serverBaseUrl}`)
} else {
console.log(
`Could not connect to a KEIP Controller at ${keipClient.serverBaseUrl}`
)
}
}

const useKeipClientStatus = () => {
const [isAvailable, setIsAvailable] = useState(false)

useEffect(() => {
void (async () => {
const status = await keipClient.ping()
logKeipClientStatus(status)
setIsAvailable(status)
})()
}, [])

return isAvailable
}

export default useKeipClientStatus
44 changes: 44 additions & 0 deletions ui/src/components/endpoints/translation/translateFlowToXml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { EipFlow } from "../../../api/generated/eipFlow"

// highlight.js theme
import "highlight.js/styles/intellij-light.css"
import { FLOW_TRANSLATOR_BASE_URL } from "../../../singletons/externalEndpoints"
import fetchWithTimeout from "../../../utils/fetch/fetchWithTimeout"

interface FlowTranslationResponse {
data?: string
error?: {
message: string
type: string
details: object[]
}
}

// TODO: Add client-side caching (might make sense to use a data fetching library)
export const fetchXmlTranslation = async (
flow: EipFlow,
abortCtrl: AbortController
) => {
const queryStr = new URLSearchParams({ prettyPrint: "true" }).toString()
const response = await fetchWithTimeout(
`${FLOW_TRANSLATOR_BASE_URL}/translation/toSpringXml?` + queryStr,
{
method: "POST",
body: JSON.stringify(flow),
headers: {
"Content-Type": "application/json",
},
timeout: 20000,
abortCtrl,
}
)

const { data, error } = (await response.json()) as FlowTranslationResponse

if (!response.ok) {
console.error("Failed to convert diagram to XML:", error)
throw new Error(JSON.stringify(error))
}

return data!
}
14 changes: 14 additions & 0 deletions ui/src/components/options-menu/OptionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { OverflowMenu, OverflowMenuItem } from "@carbon/react"
import { Menu } from "@carbon/react/icons"
import { useState } from "react"
import useKeipClientStatus from "../endpoints/deploy/keipClientStatusHook"
import useTranslationServerStatus from "../endpoints/translation/translatorStatusHook"
import ExportPng from "./ExportPng"
import SaveDiagram from "./SaveDiagram"
import { DeployRouteModal } from "./modals/DeployRouteModal"
import { ImportFlowModal } from "./modals/ImportFlowModal"

const OptionsMenu = () => {
const [importFlowModalOpen, setImportFlowModalOpen] = useState(false)
const [deployModalOpen, setDeployModalOpen] = useState(false)
const isKeipWebhookAvailable = useKeipClientStatus()
const isTranslationServerAvailable = useTranslationServerStatus()

return (
<>
Expand All @@ -26,13 +32,21 @@ const OptionsMenu = () => {
itemText="Import Flow JSON"
onClick={() => setImportFlowModalOpen(true)}
/>

{/* Route deployment requires both the flow translator and Keip controller endpoints to be available */}
<OverflowMenuItem
itemText="Deploy Route"
onClick={() => setDeployModalOpen(true)}
disabled={!isKeipWebhookAvailable || !isTranslationServerAvailable}
/>
</OverflowMenu>

{/* Modals */}
<ImportFlowModal
open={importFlowModalOpen}
setOpen={setImportFlowModalOpen}
/>
<DeployRouteModal open={deployModalOpen} setOpen={setDeployModalOpen} />
</>
)
}
Expand Down
106 changes: 106 additions & 0 deletions ui/src/components/options-menu/modals/DeployRouteModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Modal, Stack, TextInput } from "@carbon/react"
import { InlineLoadingStatus } from "@carbon/react/lib/components/InlineLoading/InlineLoading"
import { useState } from "react"
import { createPortal } from "react-dom"
import { getEipFlow } from "../../../singletons/store/storeViews"
import { keipClient } from "../../endpoints/deploy/keipClient"
import { fetchXmlTranslation } from "../../endpoints/translation/translateFlowToXml"

interface DeployRouteModalProps {
open: boolean
setOpen: (open: boolean) => void
}

const getLoadingDescription = (status: InlineLoadingStatus) => {
switch (status) {
case "active":
return "Deploying Route"
case "error":
return "Deploy Failed"
case "finished":
return "Deployed"
case "inactive":
return ""
}
}

// TODO: Display status for deployed routes once Keip controller endpoint adds support for status checks.
// TODO: Store deployed route name and namespace in the AppStore to survive refreshes.
export const DeployRouteModal = ({ open, setOpen }: DeployRouteModalProps) => {
const [loadingStatus, setLoadingStatus] =
useState<InlineLoadingStatus>("inactive")

const [name, setName] = useState("")
const [namespace, setNamespace] = useState("default")

const onClose = () => {
setOpen(false)
loadingStatus !== "active" && setLoadingStatus("inactive")
}

const handleDeploy = () => {
setLoadingStatus("active")
deployDiagram()
.then(() => setLoadingStatus("finished"))
.catch((err) => {
console.error(err)
setLoadingStatus("error")
})
}

const deployDiagram = async () => {
const flow = getEipFlow()

if (flow.nodes?.length === 0) {
throw new Error("Failed to deploy - diagram is empty")
}

const xml = await fetchXmlTranslation(flow, new AbortController())

if (!xml) {
throw new Error("Failed to deploy - translated XML is empty")
}

const request = {
routes: [
{
name,
namespace,
xml,
},
],
}

await keipClient.deploy(request)
}

return createPortal(
<Modal
loadingDescription={getLoadingDescription(loadingStatus)}
loadingStatus={loadingStatus}
modalHeading="Deploy Routes"
onRequestClose={onClose}
onRequestSubmit={handleDeploy}
open={open}
primaryButtonText="Deploy"
secondaryButtonText="Cancel"
>
<Stack gap={6}>
<TextInput
id="name-input"
labelText="Name"
placeholder="Route name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextInput
id="namespace-input"
labelText="Namespace"
value={namespace}
onChange={(e) => setNamespace(e.target.value)}
/>
</Stack>
</Modal>,
document.body
)
}
2 changes: 1 addition & 1 deletion ui/src/components/toolbar/ToolbarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { KEIP_ASSISTANT_DOCS_URL } from "../../singletons/externalEndpoints"
import AssistantChatPanel from "./assistant/AssistantChatPanel"
import { useLlmServerStatus } from "./assistant/llmStatusHook"
import XmlPanel from "./xml/XmlPanel"
import useTranslationServerStatus from "./xml/translatorStatusHook"
import useTranslationServerStatus from "../endpoints/translation/translatorStatusHook"

interface PanelButtonProps {
name: string
Expand Down
42 changes: 1 addition & 41 deletions ui/src/components/toolbar/xml/XmlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,16 @@ import hljs from "highlight.js/lib/core"
import xml from "highlight.js/lib/languages/xml"
import { useEffect, useState } from "react"
import Editor from "react-simple-code-editor"
import { EipFlow } from "../../../api/generated/eipFlow"
import { useEipFlow } from "../../../singletons/store/diagramToEipFlow"

// highlight.js theme
import "highlight.js/styles/intellij-light.css"
import { FLOW_TRANSLATOR_BASE_URL } from "../../../singletons/externalEndpoints"
import fetchWithTimeout from "../../../utils/fetch/fetchWithTimeout"
import { fetchXmlTranslation } from "../../endpoints/translation/translateFlowToXml"

const UNMOUNT_SIGNAL = "unmount"

hljs.registerLanguage("xml", xml)

interface FlowTranslationResponse {
data?: string
error?: {
message: string
type: string
details: object[]
}
}

type InlineLoadingProps = React.ComponentProps<typeof InlineLoading>

const getLoadingStatus = (
Expand All @@ -38,35 +27,6 @@ const getLoadingStatus = (
: { status: "finished", description: "synced" }
}

// TODO: Add client-side caching (might make sense to use a data fetching library)
const fetchXmlTranslation = async (
flow: EipFlow,
abortCtrl: AbortController
) => {
const queryStr = new URLSearchParams({ prettyPrint: "true" }).toString()
const response = await fetchWithTimeout(
`${FLOW_TRANSLATOR_BASE_URL}/translation/toSpringXml?` + queryStr,
{
method: "POST",
body: JSON.stringify(flow),
headers: {
"Content-Type": "application/json",
},
timeout: 20000,
abortCtrl,
}
)

const { data, error } = (await response.json()) as FlowTranslationResponse

if (!response.ok) {
console.error("Failed to convert diagram to XML:", error)
throw new Error(JSON.stringify(error))
}

return data!
}

const XmlPanel = () => {
const [content, setContent] = useState("")
const [isLoading, setLoading] = useState(false)
Expand Down
2 changes: 2 additions & 0 deletions ui/src/singletons/externalEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export const KEIP_ASSISTANT_DOCS_URL = import.meta.env

export const KEIP_ASSISTANT_OLLAMA_URL = import.meta.env
.VITE_KEIP_ASSISTANT_OLLAMA_URL

export const K8S_CLUSTER_URL = import.meta.env.VITE_KEIP_K8S_CLUSTER_URL
Loading