This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
commit 4dd412f91ac73346b079121b7029c548c781a896 Author: Marat Gubaidullin <marat.gubaidul...@gmail.com> AuthorDate: Sun Jul 23 14:51:25 2023 -0400 Dashboard supports containers in Docker #817 --- .../camel/karavan/api/InfrastructureResource.java | 13 + .../src/main/resources/application.properties | 2 +- .../karavan-app/src/main/webui/src/Main.tsx | 27 +- .../src/main/webui/src/api/KaravanApi.tsx | 11 + .../src/main/webui/src/api/ProjectService.ts | 17 +- .../src/main/webui/src/api/ProjectStore.ts | 56 ++- .../src/main/webui/src/dashboard/DashboardPage.tsx | 484 +++++++++++---------- .../src/main/webui/src/projects/ProjectsPage.tsx | 20 +- .../main/webui/src/projects/ProjectsTableRow.tsx | 15 +- .../karavan/infinispan/InfinispanService.java | 4 + 10 files changed, 382 insertions(+), 267 deletions(-) diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java index 2a7ec6c2..443c9e69 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java @@ -141,6 +141,19 @@ public class InfrastructureResource { .collect(Collectors.toList()); } + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/container") + public List<ContainerStatus> getAllContainerStatuses() throws Exception { + if (infinispanService.isReady()) { + return infinispanService.getContainerStatuses().stream() + .sorted(Comparator.comparing(ContainerStatus::getProjectId)) + .collect(Collectors.toList()); + } else { + return List.of(); + } + } + @GET @Produces(MediaType.APPLICATION_JSON) @Path("/pod/{env}") diff --git a/karavan-web/karavan-app/src/main/resources/application.properties b/karavan-web/karavan-app/src/main/resources/application.properties index 2cee473c..37a78899 100644 --- a/karavan-web/karavan-app/src/main/resources/application.properties +++ b/karavan-web/karavan-app/src/main/resources/application.properties @@ -1,6 +1,6 @@ karavan.version=3.21.1-SNAPSHOT karavan.environment=dev -karavan.environments=dev,test,prod +karavan.environments=dev karavan.default-runtime=quarkus karavan.runtimes=quarkus,spring-boot karavan.camel.status.interval=off diff --git a/karavan-web/karavan-app/src/main/webui/src/Main.tsx b/karavan-web/karavan-app/src/main/webui/src/Main.tsx index 11bf54f0..9e36ffa3 100644 --- a/karavan-web/karavan-app/src/main/webui/src/Main.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/Main.tsx @@ -50,11 +50,7 @@ export const Main = () => { const [config, setConfig] = useAppConfigStore((state) => [state.config, state.setConfig], shallow) const [pageId, setPageId] = useState<string>('projects'); - const [openapi, setOpenapi] = useState<string>(''); - const [filename, setFileName] = useState<string>(''); - const [key, setKey] = useState<string>(''); const [request, setRequest] = useState<string>(uuidv4()); - const [isModalOpen, setIsModelOpen] = useState<boolean>(false); const [showUser, setShowUser] = useState<boolean>(false); useEffect(() => { @@ -71,8 +67,6 @@ export const Main = () => { getData(); }); }); - } else { - setKey(Math.random().toString()) } if (KaravanApi.isAuthorized || KaravanApi.authType === 'public') { getData(); @@ -138,15 +132,22 @@ export const Main = () => { }); } - function pageNav() { + function getMenu() : MenuItem[] { const pages: MenuItem[] = [ new MenuItem("dashboard", "Dashboard", <DashboardIcon/>), new MenuItem("projects", "Projects", <ProjectsIcon/>), - new MenuItem("services", "Services", <ServicesIcon/>), - new MenuItem("containers", "Containers", <ContainersIcon/>), - new MenuItem("knowledgebase", "Knowledgebase", <KnowledgebaseIcon/>), - // new MenuItem("components", "Components", <ComponentsIcon/>) ] + if (config.infrastructure === 'docker') { + pages.push( + new MenuItem("services", "Services", <ServicesIcon/>), + new MenuItem("containers", "Containers", <ContainersIcon/>) + ) + } + pages.push(new MenuItem("knowledgebase", "Knowledgebase", <KnowledgebaseIcon/>)); + return pages; + } + + function pageNav() { return (<Flex className="nav-buttons" direction={{default: "column"}} style={{height: "100%"}} spaceItems={{default: "spaceItemsNone"}}> <FlexItem alignSelf={{default: "alignSelfCenter"}}> @@ -155,7 +156,7 @@ export const Main = () => { {Icon()} </Tooltip> </FlexItem> - {pages.map(page => + {getMenu().map(page => <FlexItem key={page.pageId} className={pageId === page.pageId ? "nav-button-selected" : ""}> <Tooltip content={page.tooltip} position={"right"}> <Button id={page.pageId} icon={page.icon} variant={"plain"} @@ -211,7 +212,7 @@ export const Main = () => { {pageNav()} </FlexItem> <FlexItem flex={{default: "flex_2"}} style={{height: "100%"}}> - {pageId === 'dashboard' && <DashboardPage key={request} toast={toast} config={config}/>} + {pageId === 'dashboard' && <DashboardPage key='dashboard'/>} {pageId === 'projects' && <ProjectsPage key={request}/>} {pageId === 'project' && <ProjectPage key="projects"/>} {pageId === 'services' && <ServicesPage key="services"/>} diff --git a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx index 6515e83a..7a964268 100644 --- a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx @@ -401,6 +401,17 @@ export class KaravanApi { }); } + static async getAllContainerStatuses(after: (statuses: ContainerStatus[]) => void) { + instance.get('/api/infrastructure/container') + .then(res => { + if (res.status === 200) { + after(res.data); + } + }).catch(err => { + console.log(err); + }); + } + static async getAllDeploymentStatuses(after: (statuses: DeploymentStatus[]) => void) { instance.get('/api/infrastructure/deployment') .then(res => { diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts index 4e6621cc..4fbd9da7 100644 --- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts +++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts @@ -4,9 +4,8 @@ import {TemplateApi} from "karavan-core/lib/api/TemplateApi"; import {InfrastructureAPI} from "../designer/utils/InfrastructureAPI"; import {unstable_batchedUpdates} from 'react-dom' import { - useAppConfigStore, - useDeploymentStatusesStore, useFilesStore, + useStatusesStore, useFileStore, useLogStore, useProjectsStore, useProjectStore, useDevModeStore @@ -123,9 +122,21 @@ export class ProjectService { }); } + public static refreshAllContainerStatuses() { + KaravanApi.getAllContainerStatuses( (statuses: ContainerStatus[]) => { + useStatusesStore.setState({containers: statuses}); + }); + } + + public static refreshAllDeploymentStatuses() { + KaravanApi.getAllDeploymentStatuses( (statuses: DeploymentStatus[]) => { + useStatusesStore.setState({deployments: statuses}); + }); + } + public static refreshDeploymentStatuses(environment: string) { KaravanApi.getDeploymentStatuses(environment, (statuses: DeploymentStatus[]) => { - useDeploymentStatusesStore.setState({statuses: statuses}); + useStatusesStore.setState({deployments: statuses}); }); } diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts index b4930ead..418d1848 100644 --- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts +++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts @@ -16,10 +16,9 @@ */ import {create} from 'zustand' -import {AppConfig, DeploymentStatus, ContainerStatus, Project, ProjectFile, ToastMessage} from "./ProjectModels"; +import {AppConfig, DeploymentStatus, ContainerStatus, Project, ProjectFile, ServiceStatus, CamelStatus} from "./ProjectModels"; import {ProjectEventBus} from "./ProjectEventBus"; import {unstable_batchedUpdates} from "react-dom"; -import {bottom} from "@patternfly/react-core/helpers/Popper/thirdparty/popper-core"; interface AppConfigState { config: AppConfig; @@ -136,21 +135,6 @@ export const useFileStore = create<FileState>((set) => ({ }, })) -interface DeploymentStatusesState { - statuses: DeploymentStatus[]; - setDeploymentStatuses: (statuses: DeploymentStatus[]) => void; -} - -export const useDeploymentStatusesStore = create<DeploymentStatusesState>((set) => ({ - statuses: [], - setDeploymentStatuses: (statuses: DeploymentStatus[]) => { - set((state: DeploymentStatusesState) => ({ - statuses: statuses - })); - }, -})) - - interface DevModeState { podName?: string, status: "none" | "starting" | "deleting"| "reloading" | "running", @@ -167,6 +151,44 @@ export const useDevModeStore = create<DevModeState>((set) => ({ }, })) +interface StatusesState { + deployments: DeploymentStatus[]; + services: ServiceStatus[]; + containers: ContainerStatus[]; + camels: CamelStatus[]; + setDeployments: (d: DeploymentStatus[]) => void; + setServices: (s: ServiceStatus[]) => void; + setContainers: (c: ContainerStatus[]) => void; + setCamels: (c: CamelStatus[]) => void; +} + +export const useStatusesStore = create<StatusesState>((set) => ({ + deployments: [], + services: [], + containers: [], + camels: [], + setDeployments: (d: DeploymentStatus[]) => { + set((state: StatusesState) => ({ + deployments: d, + })); + }, + setServices: (s: ServiceStatus[]) => { + set((state: StatusesState) => ({ + services: s, + })); + }, + setContainers: (c: ContainerStatus[]) => { + set((state: StatusesState) => ({ + containers: c, + })); + }, + setCamels: (c: CamelStatus[]) => { + set((state: StatusesState) => ({ + camels: c, + })); + } +})) + interface LogState { podName?: string, isRunning: boolean, diff --git a/karavan-web/karavan-app/src/main/webui/src/dashboard/DashboardPage.tsx b/karavan-web/karavan-app/src/main/webui/src/dashboard/DashboardPage.tsx index 285508d3..4f2cc9ca 100644 --- a/karavan-web/karavan-app/src/main/webui/src/dashboard/DashboardPage.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/dashboard/DashboardPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect, useState} from 'react'; import { Badge, Bullseye, Button, EmptyState, EmptyStateIcon, EmptyStateVariant, @@ -13,7 +13,7 @@ import { ToolbarItem, Tooltip } from '@patternfly/react-core'; import '../designer/karavan.css'; -import {CamelStatus, DeploymentStatus, Project, ServiceStatus} from "../api/ProjectModels"; +import {CamelStatus, ContainerStatus, DeploymentStatus, Project, ServiceStatus} from "../api/ProjectModels"; import {TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; import {camelIcon, CamelUi} from "../designer/utils/CamelUi"; import {KaravanApi} from "../api/KaravanApi"; @@ -23,177 +23,158 @@ import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon import RefreshIcon from "@patternfly/react-icons/dist/esm/icons/sync-alt-icon"; import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon"; import {MainToolbar} from "../designer/MainToolbar"; +import {useAppConfigStore, useProjectsStore, useStatusesStore} from "../api/ProjectStore"; +import {shallow} from "zustand/shallow"; -interface Props { - config: any, - toast: (title: string, text: string, variant: 'success' | 'danger' | 'warning' | 'info' | 'default') => void -} +export const DashboardPage = () => { -interface State { - projects: Project[], - deploymentStatuses: DeploymentStatus[], - serviceStatuses: ServiceStatus[], - camelStatuses: CamelStatus[], - isCreateModalOpen: boolean, - isDeleteModalOpen: boolean, - isCopy: boolean, - loading: boolean, - projectToCopy?: Project, - projectToDelete?: Project, - filter: string, - name: string, - description: string, - projectId: string, - selectedEnv: string[] -} + const [config] = useAppConfigStore((state) => [state.config], shallow) + const [projects, setProjects] = useProjectsStore((state) => [state.projects, state.setProjects], shallow) + const [deployments, services, containers, camels, setDeployments, setServices, setContainers, setCamels] + = useStatusesStore((state) => [state.deployments, state.services, state.containers, state.camels, + state.setDeployments, state.setServices, state.setContainers, state.setCamels], shallow); + const [filter, setFilter] = useState<string>(''); + const [loading, setLoading] = useState<boolean>(true); + const [selectedEnv, setSelectedEnv] = useState<string[]>([config.environment]); -export class DashboardPage extends React.Component<Props, State> { + useEffect(() => { + const interval = setInterval(() => { + onGetProjects() + }, 1300); + return () => { + clearInterval(interval) + }; + }, []); - public state: State = { - projects: [], - deploymentStatuses: [], - serviceStatuses: [], - camelStatuses: [], - isCreateModalOpen: false, - isDeleteModalOpen: false, - isCopy: false, - loading: true, - filter: '', - name: '', - description: '', - projectId: '', - selectedEnv: this.getEnvironments() - }; - interval: any; - - componentDidMount() { - this.interval = setInterval(() => this.onGetProjects(), 1300); - } - - componentWillUnmount() { - clearInterval(this.interval); - } - - onGetProjects = () => { + function onGetProjects() { KaravanApi.getConfiguration((config: any) => { KaravanApi.getProjects((projects: Project[]) => { - this.setState({projects: projects, loading: false}) + setProjects(projects); }); KaravanApi.getAllDeploymentStatuses((statuses: DeploymentStatus[]) => { - this.setState({deploymentStatuses: statuses}); + setDeployments(statuses); }); KaravanApi.getAllServiceStatuses((statuses: ServiceStatus[]) => { - this.setState({serviceStatuses: statuses}); + setServices(statuses); }); - this.getSelectedEnvironments().forEach(env => { - KaravanApi.getAllCamelStatuses(env,(statuses: CamelStatus[]) => { - this.setState((state) => { - statuses.forEach(newStatus => { - const index = state.camelStatuses.findIndex(s => s.projectId === newStatus.projectId && s.env === newStatus.env); - if (index !== -1) { - state.camelStatuses.splice(index, 1); - } - state.camelStatuses.push(newStatus); - }) - return state; - }) + KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => { + setContainers(statuses); + }); + selectedEnv.forEach(env => { + KaravanApi.getAllCamelStatuses(env, (statuses: CamelStatus[]) => { + setCamels(statuses); + // setState((state) => { + // statuses.forEach(newStatus => { + // const index = state.camelStatuses.findIndex(s => s.projectId === newStatus.projectId && s.env === newStatus.env); + // if (index !== -1) { + // state.camelStatuses.splice(index, 1); + // } + // state.camelStatuses.push(newStatus); + // }) + // return state; + // }) }); - }) + }); + setLoading(false); }); } - selectEnvironment(name: string, selected: boolean) { - if (selected && !this.state.selectedEnv.includes(name)) { - this.setState((state) => { - state.selectedEnv.push(name); + function selectEnvironment(name: string, selected: boolean) { + if (selected && !selectedEnv.includes(name)) { + setSelectedEnv((state: string[]) => { + state.push(name); return state; }) - } else if (!selected && this.state.selectedEnv.includes(name)) { - this.setState((prevState) => ({ - selectedEnv: prevState.selectedEnv.filter(e => e !== name) - })); + } else if (!selected && selectedEnv.includes(name)) { + setSelectedEnv((state: string[]) => { + return state.filter(e => e !== name) + }) } } - tools = () => (<Toolbar id="toolbar-group-types"> - <ToolbarContent> - <ToolbarItem> - <Button variant="link" icon={<RefreshIcon/>} onClick={e => this.onGetProjects()}/> - </ToolbarItem> - <ToolbarItem> - <ToggleGroup aria-label="Default with single selectable"> - {this.getEnvironments().map(env => ( - <ToggleGroupItem key={env} text={env} buttonId={env} isSelected={this.state.selectedEnv.includes(env)} onChange={selected => this.selectEnvironment(env, selected)}/> - ))} - </ToggleGroup> - </ToolbarItem> - <ToolbarItem> - <TextInput className="text-field" type="search" id="search" name="search" - autoComplete="off" placeholder="Search deployment by name" - value={this.state.filter} - onChange={e => this.setState({filter: e})}/> - </ToolbarItem> - </ToolbarContent> - </Toolbar>); - - title = () => (<TextContent> - <Text component="h2">Dashboard</Text> - </TextContent>); + function tools() { + return (<Toolbar id="toolbar-group-types"> + <ToolbarContent> + <ToolbarItem> + <Button variant="link" icon={<RefreshIcon/>} onClick={e => onGetProjects()}/> + </ToolbarItem> + <ToolbarItem> + <ToggleGroup aria-label="Default with single selectable"> + {config.environments.map(env => ( + <ToggleGroupItem key={env} text={env} buttonId={env} isSelected={selectedEnv.includes(env)} onChange={selected => selectEnvironment(env, selected)}/> + ))} + </ToggleGroup> + </ToolbarItem> + <ToolbarItem> + <TextInput className="text-field" type="search" id="search" name="search" + autoComplete="off" placeholder="Search deployment by name" + value={filter} + onChange={e => setFilter(e)}/> + </ToolbarItem> + </ToolbarContent> + </Toolbar>); + } - getEnvironments(): string [] { - return this.props.config.environments && Array.isArray(this.props.config.environments) ? Array.from(this.props.config.environments) : []; + function title() { + return (<TextContent> + <Text component="h2">Dashboard</Text> + </TextContent>); } - getSelectedEnvironments(): string [] { - return this.getEnvironments().filter(e => this.state.selectedEnv.includes(e)); + function getSelectedEnvironments(): string [] { + return config.environments.filter(e => selectedEnv.includes(e)); } - getDeploymentEnvironments(name: string): [string, boolean] [] { - const deps = this.state.deploymentStatuses; - return this.getSelectedEnvironments().map(e => { + function getDeploymentEnvironments(name: string): [string, boolean] [] { + return selectedEnv.map(e => { const env: string = e as string; - const dep = deps.find(d => d.name === name && d.env === env); + const dep = deployments.find(d => d.name === name && d.env === env); const deployed: boolean = dep !== undefined && dep.replicas > 0 && dep.replicas === dep.readyReplicas; return [env, deployed]; }); } - getDeploymentByEnvironments(name: string): [string, DeploymentStatus | undefined] [] { - const deps = this.state.deploymentStatuses; - return this.getSelectedEnvironments().map(e => { + function getDeploymentByEnvironments(name: string): [string, DeploymentStatus | undefined] [] { + return selectedEnv.map(e => { const env: string = e as string; - const dep = deps.find(d => d.name === name && d.env === env); + const dep = deployments.find(d => d.name === name && d.env === env); return [env, dep]; }); } - getServiceByEnvironments(name: string): [string, ServiceStatus | undefined] [] { - const services = this.state.serviceStatuses; - return this.getSelectedEnvironments().map(e => { + function getServiceByEnvironments(name: string): [string, ServiceStatus | undefined] [] { + return selectedEnv.map(e => { const env: string = e as string; const service = services.find(d => d.name === name && d.env === env); return [env, service]; }); } - getCamelStatusByEnvironments(name: string): [string, CamelStatus | undefined] [] { - const camelStatuses = this.state.camelStatuses; - return this.getSelectedEnvironments().map(e => { + function getContainerByEnvironments(name: string): [string, ContainerStatus | undefined] [] { + return selectedEnv.map(e => { + const env: string = e as string; + const container = containers.find(d => d.containerName === name && d.env === env); + return [env, container]; + }); + } + + function getCamelStatusByEnvironments(name: string): [string, CamelStatus | undefined] [] { + return getSelectedEnvironments().map(e => { const env: string = e as string; - const status = camelStatuses.find(d => d.projectId === name && d.env === env); + const status = camels.find(d => d.projectId === name && d.env === env); return [env, status]; }); } - getProject(name: string): Project | undefined { - return this.state.projects.filter(p => p.projectId === name)?.at(0); + function getProject(name: string): Project | undefined { + return projects.filter(p => p.projectId === name)?.at(0); } - isKaravan(name: string): boolean { - return this.state.projects.findIndex(p => p.projectId === name) > -1; + function isKaravan(name: string): boolean { + return projects.findIndex(p => p.projectId === name) > -1; } - getReplicasPanel(deploymentStatus?: DeploymentStatus) { + function getReplicasPanel(deploymentStatus?: DeploymentStatus) { if (deploymentStatus) { const readyReplicas = deploymentStatus.readyReplicas ? deploymentStatus.readyReplicas : 0; const ok = (deploymentStatus && readyReplicas > 0 @@ -225,8 +206,7 @@ export class DashboardPage extends React.Component<Props, State> { } } - getEmptyState() { - const {loading} = this.state; + function getEmptyState() { return ( <Tr> <Td colSpan={8}> @@ -246,103 +226,167 @@ export class DashboardPage extends React.Component<Props, State> { ) } - render() { - const deployments = Array.from(new Set(this.state.deploymentStatuses.filter(d => d.name.toLowerCase().includes(this.state.filter)).map(d => d.name))); + function getKubernetesTable() { + const deps = Array.from(new Set(deployments.filter(d => d.name.toLowerCase().includes(filter)).map(d => d.name))); return ( - <PageSection className="kamelet-section dashboard-page" padding={{default: 'noPadding'}}> - <PageSection className="tools-section" padding={{default: 'noPadding'}}> - <MainToolbar title={this.title()} tools={this.tools()}/> - </PageSection> - <PageSection isFilled className="kamelets-page"> - <TableComposable aria-label="Projects" variant={TableVariant.compact}> - <Thead> - <Tr> - <Th key='type'>Type</Th> - <Th key='name'>Deployment</Th> - <Th key='description'>Project/Description</Th> - <Th key='environment'>Environment</Th> - <Th key='namespace'>Namespace</Th> - <Th key='replicas'>Replicas</Th> - <Th key='services'>Services</Th> - <Th key='camel'>Camel Health</Th> - {/*<Th key='action'></Th>*/} - </Tr> - </Thead> - <Tbody> - {deployments.map(deployment => ( - <Tr key={deployment}> - <Td style={{verticalAlign: "middle"}}> - {this.isKaravan(deployment) ? Icon("icon") : CamelUi.getIconFromSource(camelIcon)} - </Td> - <Td style={{verticalAlign: "middle"}}> - <Button style={{padding: '6px'}} variant={"link"}>{deployment}</Button> - </Td> - <Td style={{verticalAlign: "middle"}}> - <HelperText> - <HelperTextItem>{this.getProject(deployment)?.name || ""}</HelperTextItem> - <HelperTextItem>{this.getProject(deployment)?.description || "Camel project"}</HelperTextItem> - </HelperText> - </Td> - <Td> - <Flex direction={{default: "column"}}> - {this.getDeploymentEnvironments(deployment).map(value => ( - <FlexItem className="badge-flex-item" key={value[0]}><Badge className="badge" - isRead={!value[1]}>{value[0]}</Badge></FlexItem> - ))} - </Flex> - </Td> - <Td> - <Flex direction={{default: "column"}}> - {this.getDeploymentByEnvironments(deployment).map(value => ( - <FlexItem className="badge-flex-item" key={value[0]}> - <Label variant={"outline"}> - {value[1]?.namespace || "???"} - </Label> - </FlexItem> - ))} - </Flex> - </Td> - <Td> - <Flex direction={{default: "column"}}> - {this.getDeploymentByEnvironments(deployment).map(value => ( - <FlexItem className="badge-flex-item" key={value[0]}>{this.getReplicasPanel(value[1])}</FlexItem> - ))} - </Flex> - </Td> - <Td> - <Flex direction={{default: "column"}}> - {this.getServiceByEnvironments(deployment).map(value => ( - <FlexItem className="badge-flex-item" key={value[0]}> - <Label variant={"outline"}> - {value[1] ? (value[1]?.port + " -> " + value[1]?.targetPort) : "???"} - </Label> - </FlexItem> - ))} - </Flex> - </Td> - <Td modifier={"fitContent"}> - <Flex direction={{default: "column"}}> - {this.getCamelStatusByEnvironments(deployment).map(value => { - // const color = value[1] ? (value[1].consumerStatus === "UP" ? "green" : "red") : "grey"; - // let icon = undefined; - // if (value[1]?.consumerStatus === "UP") icon = <UpIcon/> - // if (value[1]?.consumerStatus === "DOWN") icon = <DownIcon/> - // const text = value[1] && value[1]?.contextVersion ? value[1]?.contextVersion : "???"; - return <FlexItem key={value[0]}> - {/*<LabelGroup numLabels={4} className="camel-label-group">*/} - {/* <Label color={color} className="table-label" icon={icon}>{text}</Label>*/} - {/*</LabelGroup>*/} - </FlexItem> - })} - </Flex> - </Td> - </Tr> - ))} - {deployments.length === 0 && this.getEmptyState()} - </Tbody> - </TableComposable> - </PageSection> - </PageSection> + <TableComposable aria-label="Projects" variant={TableVariant.compact}> + <Thead> + <Tr> + <Th key='type'>Type</Th> + <Th key='name'>Deployment</Th> + <Th key='description'>Project/Description</Th> + <Th key='environment'>Environment</Th> + <Th key='namespace'>Namespace</Th> + <Th key='replicas'>Replicas</Th> + <Th key='services'>Services</Th> + <Th key='camel'>Camel Health</Th> + {/*<Th key='action'></Th>*/} + </Tr> + </Thead> + <Tbody> + {deps.map(deployment => ( + <Tr key={deployment}> + <Td style={{verticalAlign: "middle"}}> + {isKaravan(deployment) ? Icon("icon") : CamelUi.getIconFromSource(camelIcon)} + </Td> + <Td style={{verticalAlign: "middle"}}> + <Button style={{padding: '6px'}} variant={"link"}>{deployment}</Button> + </Td> + <Td style={{verticalAlign: "middle"}}> + <HelperText> + <HelperTextItem>{getProject(deployment)?.name || ""}</HelperTextItem> + <HelperTextItem>{getProject(deployment)?.description || "Camel project"}</HelperTextItem> + </HelperText> + </Td> + <Td> + <Flex direction={{default: "column"}}> + {getDeploymentEnvironments(deployment).map(value => ( + <FlexItem className="badge-flex-item" key={value[0]}><Badge className="badge" + isRead={!value[1]}>{value[0]}</Badge></FlexItem> + ))} + </Flex> + </Td> + <Td> + <Flex direction={{default: "column"}}> + {getServiceByEnvironments(deployment).map(value => ( + <FlexItem className="badge-flex-item" key={value[0]}> + <Label variant={"outline"}> + {value[1] ? (value[1]?.port + " -> " + value[1]?.targetPort) : "???"} + </Label> + </FlexItem> + ))} + </Flex> + </Td> + <Td modifier={"fitContent"}> + <Flex direction={{default: "column"}}> + {getCamelStatusByEnvironments(deployment).map(value => { + // const color = value[1] ? (value[1].consumerStatus === "UP" ? "green" : "red") : "grey"; + // let icon = undefined; + // if (value[1]?.consumerStatus === "UP") icon = <UpIcon/> + // if (value[1]?.consumerStatus === "DOWN") icon = <DownIcon/> + // const text = value[1] && value[1]?.contextVersion ? value[1]?.contextVersion : "???"; + return <FlexItem key={value[0]}> + {/*<LabelGroup numLabels={4} className="camel-label-group">*/} + {/* <Label color={color} className="table-label" icon={icon}>{text}</Label>*/} + {/*</LabelGroup>*/} + </FlexItem> + })} + </Flex> + </Td> + </Tr> + ))} + {deps.length === 0 && getEmptyState()} + </Tbody> + </TableComposable> + ) + } + + function getDockerTable() { + const conts = containers + .filter(c => ['devmode', 'project'].includes(c.type)) + .filter(d => d.containerName.toLowerCase().includes(filter)); + return ( + <TableComposable aria-label="Projects" variant={TableVariant.compact}> + <Thead> + <Tr> + <Th key='type'>Type</Th> + <Th key='container'>Container</Th> + <Th key='description'>Project Description</Th> + <Th key='ports'>Ports</Th> + <Th key='environment'>Environment</Th> + <Th key='camel'>Camel Health</Th> + {/*<Th key='action'></Th>*/} + </Tr> + </Thead> + <Tbody> + {conts.map(container => ( + <Tr key={container.containerName}> + <Td style={{verticalAlign: "middle"}} modifier={"fitContent"}> + <Label variant={"outline"}>{container.type}</Label> + </Td> + <Td style={{verticalAlign: "middle"}}> + <Label color={container.lifeCycle === 'ready' ? "green" : 'grey'}> + {container.containerName} + </Label> + </Td> + <Td style={{verticalAlign: "middle"}}> + <HelperText> + <HelperTextItem>{getProject(container.containerName)?.description || "Camel project"}</HelperTextItem> + </HelperText> + </Td> + <Td> + <Flex direction={{default: "column"}}> + {container.ports.map(port => ( + <FlexItem className="badge-flex-item" key={port}><Badge className="badge" + isRead={true}>{port}</Badge></FlexItem> + ))} + </Flex> + </Td> + <Td> + <Flex direction={{default: "column"}}> + {getContainerByEnvironments(container.containerName).map(value => ( + <FlexItem className="badge-flex-item" key={value[0]}> + <Badge className={"badge"}> + {value[1] ? value[1]?.env : ""} + </Badge> + </FlexItem> + ))} + </Flex> + </Td> + <Td modifier={"fitContent"}> + <Flex direction={{default: "column"}}> + {getCamelStatusByEnvironments(container.containerName).map(value => { + // const color = value[1] ? (value[1].consumerStatus === "UP" ? "green" : "red") : "grey"; + // let icon = undefined; + // if (value[1]?.consumerStatus === "UP") icon = <UpIcon/> + // if (value[1]?.consumerStatus === "DOWN") icon = <DownIcon/> + // const text = value[1] && value[1]?.contextVersion ? value[1]?.contextVersion : "???"; + return <FlexItem key={value[0]}> + {/*<LabelGroup numLabels={4} className="camel-label-group">*/} + {/* <Label color={color} className="table-label" icon={icon}>{text}</Label>*/} + {/*</LabelGroup>*/} + </FlexItem> + })} + </Flex> + </Td> + </Tr> + ))} + {conts.length === 0 && getEmptyState()} + </Tbody> + </TableComposable> ) } + + const isKubernetes = config.infrastructure === 'kubernetes'; + return ( + <PageSection className="kamelet-section dashboard-page" padding={{default: 'noPadding'}}> + <PageSection className="tools-section" padding={{default: 'noPadding'}}> + <MainToolbar title={title()} tools={tools()}/> + </PageSection> + <PageSection isFilled className="kamelets-page"> + {isKubernetes ? getKubernetesTable() : getDockerTable()} + </PageSection> + </PageSection> + ) + } \ No newline at end of file diff --git a/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsPage.tsx b/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsPage.tsx index 4f74ab8b..735f99d0 100644 --- a/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsPage.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsPage.tsx @@ -22,23 +22,28 @@ import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import {ProjectsTableRow} from "./ProjectsTableRow"; import {DeleteProjectModal} from "./DeleteProjectModal"; import {CreateProjectModal} from "./CreateProjectModal"; -import {useProjectsStore, useProjectStore} from "../api/ProjectStore"; +import {useAppConfigStore, useProjectsStore, useProjectStore} from "../api/ProjectStore"; import {ProjectService} from "../api/ProjectService"; import {MainToolbar} from "../designer/MainToolbar"; import {Project, ProjectType} from "../api/ProjectModels"; +import {shallow} from "zustand/esm/shallow"; export const ProjectsPage = () => { - const {projects, setProjects} = useProjectsStore(); - const {operation} = useProjectStore(); + const [projects] = useProjectsStore((state) => [state.projects], shallow) + const [operation] = useProjectStore((state) => [state.operation], shallow) const [filter, setFilter] = useState<string>(''); const [loading, setLoading] = useState<boolean>(false); useEffect(() => { const interval = setInterval(() => { if (projects.length === 0) setLoading(true); - if (!["create", "delete", "select", "copy"].includes(operation)) ProjectService.refreshProjects(); + if (!["create", "delete", "select", "copy"].includes(operation)) { + ProjectService.refreshProjects(); + ProjectService.refreshAllDeploymentStatuses(); + ProjectService.refreshAllContainerStatuses(); + } }, 1300); return () => { clearInterval(interval) @@ -49,8 +54,11 @@ export const ProjectsPage = () => { return <Toolbar id="toolbar-group-types"> <ToolbarContent> <ToolbarItem> - <Button variant="link" icon={<RefreshIcon/>} onClick={e => - ProjectService.refreshProjects()}/> + <Button variant="link" icon={<RefreshIcon/>} onClick={e => { + ProjectService.refreshProjects(); + ProjectService.refreshAllDeploymentStatuses(); + ProjectService.refreshAllContainerStatuses(); + }}/> </ToolbarItem> <ToolbarItem> <TextInput className="text-field" type="search" id="search" name="search" diff --git a/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx b/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx index a416035f..43b38dbf 100644 --- a/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx @@ -12,9 +12,8 @@ import CopyIcon from "@patternfly/react-icons/dist/esm/icons/copy-icon"; import {DeploymentStatus, Project} from '../api/ProjectModels'; import { useAppConfigStore, - useDeploymentStatusesStore, useLogStore, - useProjectStore, + useProjectStore, useStatusesStore, } from "../api/ProjectStore"; import {ProjectEventBus} from "../api/ProjectEventBus"; import {shallow} from "zustand/shallow"; @@ -25,7 +24,7 @@ interface Props { export const ProjectsTableRow = (props: Props) => { - const {statuses} = useDeploymentStatusesStore(); + const [deployments, containers] = useStatusesStore((state) => [state.deployments, state.containers], shallow) const {config} = useAppConfigStore(); const [setProject] = useProjectStore((state) => [state.setProject, state.setOperation], shallow); const [setShowLog] = useLogStore((state) => [state.setShowLog], shallow); @@ -34,11 +33,13 @@ export const ProjectsTableRow = (props: Props) => { return config.environments && Array.isArray(config.environments) ? Array.from(config.environments) : []; } - function getDeploymentByEnvironments(name: string): [string, DeploymentStatus | undefined] [] { + function getStatusByEnvironments(name: string): [string, any] [] { return getEnvironments().map(e => { const env: string = e as string; - const dep = statuses.find(d => d.name === name && d.env === env); - return [env, dep]; + const status = config.infrastructure === 'kubernetes' + ? deployments.find(d => d.name === name && d.env === env) + : containers.find(d => d.containerName === name && d.env === env); + return [env, status]; }); } @@ -71,7 +72,7 @@ export const ProjectsTableRow = (props: Props) => { <Td noPadding style={{width: "180px"}}> {!isBuildIn && <Flex direction={{default: "row"}}> - {getDeploymentByEnvironments(project.projectId).map(value => ( + {getStatusByEnvironments(project.projectId).map(value => ( <FlexItem className="badge-flex-item" key={value[0]}> <Badge className="badge" isRead={!value[1]}>{value[0]}</Badge> </FlexItem> diff --git a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java index 58732c4b..efd1ee2c 100644 --- a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java +++ b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java @@ -250,6 +250,10 @@ public class InfinispanService { return new ArrayList<>(serviceStatuses.values()); } + public List<ContainerStatus> getContainerStatuses() { + return new ArrayList<>(containerStatuses.values()); + } + public List<ContainerStatus> getContainerStatuses(String projectId, String env) { QueryFactory queryFactory = Search.getQueryFactory(containerStatuses); return queryFactory.<ContainerStatus>create("FROM karavan.ContainerStatus WHERE projectId = :projectId AND env = :env")