You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2023/07/23 18:52:16 UTC

[camel-karavan] 02/03: Dashboard supports containers in Docker #817

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 <ma...@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")