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/09/08 23:48:33 UTC

[camel-karavan] branch main updated (7d6280fb -> f0a5d214)

This is an automated email from the ASF dual-hosted git repository.

marat pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git


    from 7d6280fb Set image for #817
     new b9985be4 Run prod container for #817
     new 72cece8c Delete images for #817
     new 624691aa DevMode vs Run prod for #817
     new 482c50f1 Kubernetesless forst full process for #817
     new f0a5d214 Sync karavan space for #817

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 karavan-designer/src/designer/karavan.css          |   3 +-
 karavan-space/src/designer/KaravanDesigner.tsx     |  26 +-
 karavan-space/src/designer/karavan.css             |   3 +-
 .../src/designer/route/DslConnections.tsx          |   4 +-
 karavan-space/src/designer/route/DslElement.tsx    |  29 ++-
 .../src/designer/route/useRouteDesignerHook.tsx    |   7 +-
 ...ructureResource.java => ContainerResource.java} | 190 ++-------------
 .../apache/camel/karavan/api/ImagesResource.java   |  17 ++
 .../camel/karavan/api/InfrastructureResource.java  | 154 +-----------
 .../camel/karavan/docker/DockerForKaravan.java     |  26 +-
 .../apache/camel/karavan/docker/DockerService.java |  51 ++--
 .../karavan/infinispan/model/ContainerStatus.java  |  33 +++
 .../apache/camel/karavan/service/CamelService.java |  27 ++-
 .../karavan/service/ContainerStatusService.java    |  56 +++++
 .../camel/karavan/service/ProjectService.java      |  13 +-
 .../camel/karavan/service/ScheduledService.java    | 100 --------
 .../camel-main-docker-application.properties       |   3 +
 .../src/main/webui/src/api/KaravanApi.tsx          |  38 +--
 .../src/main/webui/src/api/ProjectModels.ts        |   2 +-
 .../src/main/webui/src/api/ProjectService.ts       |   4 +-
 .../src/main/webui/src/project/BuildToolbar.tsx    |  39 ++-
 .../src/main/webui/src/project/DevModeToolbar.tsx  |  33 ++-
 .../src/main/webui/src/project/ProjectPanel.tsx    |   4 +
 .../src/main/webui/src/project/ProjectToolbar.tsx  |   8 +-
 .../main/webui/src/project/build/BuildPanel.tsx    | 269 +++++++++++++++++++++
 .../main/webui/src/project/build/ImagesPanel.tsx   | 222 +++++++++++++++++
 .../webui/src/project/build/ProjectBuildTab.tsx    |   9 +-
 .../src/project/container/ContainerButtons.tsx     | 123 ++++++++++
 .../webui/src/project/container/ContainerPanel.tsx |  90 +++++++
 .../ProjectContainerTab.tsx}                       |  14 +-
 .../src/project/trace/RunnerInfoTraceModal.tsx     |   1 -
 31 files changed, 1034 insertions(+), 564 deletions(-)
 copy karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/{InfrastructureResource.java => ContainerResource.java} (52%)
 delete mode 100644 karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java
 create mode 100644 karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx
 create mode 100644 karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx
 create mode 100644 karavan-web/karavan-app/src/main/webui/src/project/container/ContainerButtons.tsx
 create mode 100644 karavan-web/karavan-app/src/main/webui/src/project/container/ContainerPanel.tsx
 copy karavan-web/karavan-app/src/main/webui/src/project/{build/ProjectBuildTab.tsx => container/ProjectContainerTab.tsx} (50%)


[camel-karavan] 05/05: Sync karavan space for #817

Posted by ma...@apache.org.
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 f0a5d2142ea649464acacd42e542872e13db9241
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Sep 8 19:48:22 2023 -0400

    Sync karavan space for #817
---
 karavan-space/src/designer/KaravanDesigner.tsx     | 26 +++++++++----------
 karavan-space/src/designer/karavan.css             |  3 ++-
 .../src/designer/route/DslConnections.tsx          |  4 +--
 karavan-space/src/designer/route/DslElement.tsx    | 29 +++++++++++-----------
 .../src/designer/route/useRouteDesignerHook.tsx    |  7 ++----
 5 files changed, 33 insertions(+), 36 deletions(-)

diff --git a/karavan-space/src/designer/KaravanDesigner.tsx b/karavan-space/src/designer/KaravanDesigner.tsx
index ed6ad193..badf8ba8 100644
--- a/karavan-space/src/designer/KaravanDesigner.tsx
+++ b/karavan-space/src/designer/KaravanDesigner.tsx
@@ -133,19 +133,19 @@ export function KaravanDesigner (props: Props) {
                     <Tab eventKey='rest' title={getTab("REST", "REST services", "rest")}></Tab>
                     <Tab eventKey='beans' title={getTab("Beans", "Beans Configuration", "beans")}></Tab>
                 </Tabs>
-                {tab === 'routes' && <Tooltip content={"Hide Log elements"}>
-                    <Switch
-                        isReversed
-                        isChecked={hideLogDSL}
-                        onChange={(_, checked) => {
-                            setHideLogDSL(checked)
-                        }}
-                        aria-label={"Hide Log"}
-                        id="hideLogDSL"
-                        name="hideLogDSL"
-                        className={"hide-log"}
-                    />
-                </Tooltip>}
+                {/*{tab === 'routes' && <Tooltip content={"Hide Log elements"}>*/}
+                {/*    <Switch*/}
+                {/*        isReversed*/}
+                {/*        isChecked={hideLogDSL}*/}
+                {/*        onChange={(_, checked) => {*/}
+                {/*            setHideLogDSL(checked)*/}
+                {/*        }}*/}
+                {/*        aria-label={"Hide Log"}*/}
+                {/*        id="hideLogDSL"*/}
+                {/*        name="hideLogDSL"*/}
+                {/*        className={"hide-log"}*/}
+                {/*    />*/}
+                {/*</Tooltip>}*/}
             </div>
             {tab === 'routes' && <RouteDesigner/>}
             {tab === 'rest' && <RestDesigner/>}
diff --git a/karavan-space/src/designer/karavan.css b/karavan-space/src/designer/karavan.css
index 11324971..bf4d9d49 100644
--- a/karavan-space/src/designer/karavan.css
+++ b/karavan-space/src/designer/karavan.css
@@ -38,7 +38,8 @@
     height: 36px;
 }
 
-.karavan .pf-v5-c-switch__input:focus ~ .pf-v5-c-switch__toggle {
+.karavan .pf-v5-c-switch__input:focus ~ .pf-v5-c-switch__toggle,
+.pf-v5-c-modal-box .pf-v5-c-switch__input:focus ~ .pf-v5-c-switch__toggle {
     outline: transparent;
     outline-offset: 0;
 }
diff --git a/karavan-space/src/designer/route/DslConnections.tsx b/karavan-space/src/designer/route/DslConnections.tsx
index 9f3578ad..c9d85ccb 100644
--- a/karavan-space/src/designer/route/DslConnections.tsx
+++ b/karavan-space/src/designer/route/DslConnections.tsx
@@ -30,8 +30,8 @@ const outgoingDefinitions: string[] = ['ToDefinition', 'KameletDefinition', 'ToD
 export function DslConnections() {
 
     const [integration] = useIntegrationStore((state) => [state.integration], shallow)
-    const [width, height, top, left] = useDesignerStore((s) =>
-        [s.width, s.height, s.top, s.left], shallow)
+    const [width, height, top, left, hideLogDSL] = useDesignerStore((s) =>
+        [s.width, s.height, s.top, s.left, s.hideLogDSL], shallow)
     const [ steps, addStep, deleteStep, clearSteps] = useConnectionsStore((s) => [s.steps, s.addStep, s.deleteStep, s.clearSteps], shallow)
 
     useEffect(() => {
diff --git a/karavan-space/src/designer/route/DslElement.tsx b/karavan-space/src/designer/route/DslElement.tsx
index f3283154..9aaf6735 100644
--- a/karavan-space/src/designer/route/DslElement.tsx
+++ b/karavan-space/src/designer/route/DslElement.tsx
@@ -51,7 +51,7 @@ export function DslElement(props: Props) {
 
     const [selectedUuids, selectedStep, showMoveConfirmation, setShowMoveConfirmation, hideLogDSL] =
         useDesignerStore((s) =>
-        [s.selectedUuids, s.selectedStep, s.showMoveConfirmation, s.setShowMoveConfirmation, s.hideLogDSL], shallow)
+            [s.selectedUuids, s.selectedStep, s.showMoveConfirmation, s.setShowMoveConfirmation, s.hideLogDSL], shallow)
     const [isDragging, setIsDragging] = useState<boolean>(false);
 
     const [isDraggedOver, setIsDraggedOver] = useState<boolean>(false);
@@ -197,20 +197,21 @@ export function DslElement(props: Props) {
     function sendPosition(el: HTMLDivElement | null) {
         const isSelected = isElementSelected();
         const isHidden = isElementHidden();
-        if (isHidden) {
-            EventBus.sendPosition("delete", props.step, props.parent, new DOMRect(), new DOMRect(), 0);
-        } else {
-            if (el) {
-                const header = Array.from(el.childNodes.values()).filter((n: any) => n.classList.contains("header"))[0];
-                if (header) {
-                    const headerIcon: any = Array.from(header.childNodes.values()).filter((n: any) => n.classList.contains("header-icon"))[0];
-                    const headerRect = headerIcon.getBoundingClientRect();
-                    const rect = el.getBoundingClientRect();
-                    if (props.step.showChildren) {
+        if (el) {
+            const header = Array.from(el.childNodes.values()).filter((n: any) => n.classList.contains("header"))[0];
+            if (header) {
+                const headerIcon: any = Array.from(header.childNodes.values()).filter((n: any) => n.classList.contains("header-icon"))[0];
+                const headerRect = headerIcon.getBoundingClientRect();
+                const rect = el.getBoundingClientRect();
+                if (props.step.showChildren) {
+                    if (isHidden) {
+                        // EventBus.sendPosition("delete", props.step, props.parent, new DOMRect(), new DOMRect(), 0);
                         EventBus.sendPosition("add", props.step, props.parent, rect, headerRect, props.position, props.inSteps, isSelected);
                     } else {
-                        EventBus.sendPosition("delete", props.step, props.parent, new DOMRect(), new DOMRect(), 0);
+                        EventBus.sendPosition("add", props.step, props.parent, rect, headerRect, props.position, props.inSteps, isSelected);
                     }
+                } else {
+                    EventBus.sendPosition("delete", props.step, props.parent, new DOMRect(), new DOMRect(), 0);
                 }
             }
         }
@@ -265,8 +266,7 @@ export function DslElement(props: Props) {
         if (!checkRequired[0]) className = className + " header-text-required";
         if (checkRequired[0]) {
             return <Text className={className}>{title}</Text>
-        }
-        else return (
+        } else return (
             <Tooltip position={"right"} className="tooltip-required-field"
                      content={checkRequired[1].map((text, i) => (<div key={i}>{text}</div>))}>
                 <Text className={className}>{title}</Text>
@@ -445,7 +445,6 @@ export function DslElement(props: Props) {
              className={className}
              ref={el => sendPosition(el)}
              style={{
-                 display: isElementHidden() ? "none" : "flex",
                  borderStyle: hasBorder() ? "dotted" : "none",
                  borderColor: isElementSelected() ? "var(--step-border-color-selected)" : "var(--step-border-color)",
                  marginTop: isInStepWithChildren() ? "16px" : "8px",
diff --git a/karavan-space/src/designer/route/useRouteDesignerHook.tsx b/karavan-space/src/designer/route/useRouteDesignerHook.tsx
index cf4c8282..4d814f1a 100644
--- a/karavan-space/src/designer/route/useRouteDesignerHook.tsx
+++ b/karavan-space/src/designer/route/useRouteDesignerHook.tsx
@@ -76,6 +76,7 @@ export function useRouteDesignerHook () {
     }
 
     const deleteElement = () =>  {
+        EventBus.sendPosition("clean", new CamelElement(""), undefined, new DOMRect(), new DOMRect(), 0);
         selectedUuids.forEach(uuidToDelete => {
             const i = CamelDefinitionApiExt.deleteStepFromIntegration(integration, uuidToDelete);
             setIntegration(i, false);
@@ -83,11 +84,7 @@ export function useRouteDesignerHook () {
             setShowDeleteConfirmation(false);
             setDeleteMessage('');
             setSelectedStep(undefined);
-            setSelectedUuids([uuidToDelete]);
-
-            const el = new CamelElement("");
-            el.uuid = uuidToDelete;
-            EventBus.sendPosition("delete", el, undefined, new DOMRect(), new DOMRect(), 0);
+            setSelectedUuids([]);
         });
     }
 


[camel-karavan] 02/05: Delete images for #817

Posted by ma...@apache.org.
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 72cece8c348bcce3f90bdb79ddd6bb6af0cabbb1
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Sep 8 18:37:20 2023 -0400

    Delete images for #817
---
 .../apache/camel/karavan/api/ImagesResource.java   |   3 +
 .../apache/camel/karavan/docker/DockerService.java |   5 +
 .../src/main/webui/src/api/KaravanApi.tsx          |  12 +
 .../main/webui/src/project/build/BuildPanel.tsx    | 297 +++++++++++++++++++++
 .../webui/src/project/build/ContainersPanel.tsx    | 112 ++++++++
 .../main/webui/src/project/build/ImagesPanel.tsx   | 222 +++++++++++++++
 6 files changed, 651 insertions(+)

diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
index fe1a9f7a..b4605110 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
@@ -26,6 +26,7 @@ import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.service.ConfigService;
 import org.apache.camel.karavan.service.ProjectService;
 import org.apache.camel.karavan.service.RegistryService;
+import org.jose4j.base64url.Base64;
 
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
@@ -73,6 +74,8 @@ public class ImagesResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/{imageName}")
     public Response deleteImage(@HeaderParam("username") String username, @PathParam("imageName") String imageName) {
+        imageName= new String(Base64.decode(imageName));
+        System.out.println(imageName);
         if (ConfigService.inKubernetes()) {
             return Response.ok().build();
         } else {
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
index 9d5ac75f..f15d5b4e 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
@@ -393,5 +393,10 @@ public class DockerService extends DockerServiceUtils {
     }
 
     public void deleteImage(String imageName) {
+        Optional<Image> image = getDockerClient().listImagesCmd().withShowAll(true).exec().stream()
+                .filter(i -> Arrays.stream(i.getRepoTags()).anyMatch(s -> Objects.equals(s, imageName))).findFirst();
+        if (image.isPresent()) {
+            getDockerClient().removeImageCmd(image.get().getId()).exec();
+        }
     }
 }
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 fd66d5cd..63573e24 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
@@ -528,6 +528,18 @@ export class KaravanApi {
         });
     }
 
+    static async deleteImage(imageName: string, after: () => void) {
+        instance.delete('/api/image/' + Buffer.from(imageName).toString('base64'))
+            .then(res => {
+                console.log(res.status)
+                if (res.status === 200) {
+                    after();
+                }
+            }).catch(err => {
+            console.log(err);
+        });
+    }
+
     static async getSecrets(after: (any: []) => void) {
         instance.get('/api/infrastructure/secrets')
             .then(res => {
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx
new file mode 100644
index 00000000..752f496c
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx
@@ -0,0 +1,297 @@
+import React, {useState} from 'react';
+import {
+    Button,
+    DescriptionList,
+    DescriptionListTerm,
+    DescriptionListGroup,
+    DescriptionListDescription, Spinner, Tooltip, Flex, FlexItem, LabelGroup, Label, Modal, Badge, CardBody, Card
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import {KaravanApi} from "../../api/KaravanApi";
+import BuildIcon from "@patternfly/react-icons/dist/esm/icons/build-icon";
+import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
+import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
+import ClockIcon from "@patternfly/react-icons/dist/esm/icons/clock-icon";
+import TagIcon from "@patternfly/react-icons/dist/esm/icons/tag-icon";
+import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon";
+import {useAppConfigStore, useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {ContainersPanel} from "./ContainersPanel";
+
+interface Props {
+    env: string,
+}
+
+export function BuildPanel (props: Props) {
+
+    const [config] = useAppConfigStore((state) => [state.config], shallow)
+    const [project] = useProjectStore((s) => [s.project], shallow);
+    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
+    const [containers, deployments, camels, pipelineStatuses] =
+        useStatusesStore((s) => [s.containers, s.deployments, s.camels, s.pipelineStatuses], shallow);
+    const [isPushing, setIsPushing] = useState<boolean>(false);
+    const [isBuilding, setIsBuilding] = useState<boolean>(false);
+    const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
+    const [deleteEntityType, setDeleteEntityType] = useState<'pod' | 'deployment' | 'build'>('pod');
+    const [deleteEntityName, setDeleteEntityName] = useState<string>();
+    const [deleteEntityEnv, setDeleteEntityEnv] = useState<string>();
+    const [tag, setTag] = useState<string>(new Date().toISOString().substring(0,19).replaceAll(':', '-'));
+
+    function deleteEntity(type: 'pod' | 'deployment' | 'build', name: string, environment: string) {
+        switch (type) {
+            case "deployment":
+                KaravanApi.deleteDeployment(environment, name, (res: any) => {
+                    // if (Array.isArray(res) && Array.from(res).length > 0)
+                    // onRefresh();
+                });
+                break;
+            case "pod":
+                KaravanApi.deleteContainer(environment, 'project', name, (res: any) => {
+                    // if (Array.isArray(res) && Array.from(res).length > 0)
+                    // onRefresh();
+                });
+                break;
+            case "build":
+                KaravanApi.stopBuild(environment, name, (res: any) => {
+                    // if (Array.isArray(res) && Array.from(res).length > 0)
+                    // onRefresh();
+                });
+                break;
+        }
+    }
+
+    function build() {
+        setIsBuilding(true);
+        setShowLog(false,'none')
+        KaravanApi.buildProject(project, tag, res => {
+            if (res.status === 200 || res.status === 201) {
+                setIsBuilding(false);
+            } else {
+                // Todo notification
+            }
+        });
+    }
+
+    function buildButton(env: string) {
+        const status = pipelineStatuses.filter(p => p.projectId === project.projectId).at(0);
+        const isRunning = status?.result === 'Running';
+        return (<Tooltip content="Start build" position={"left"}>
+            <Button isLoading={isBuilding ? true : undefined}
+                    isDisabled={isBuilding || isRunning || isPushing}
+                    size="sm"
+                    variant="secondary"
+                    className="project-button"
+                    icon={!isBuilding ? <BuildIcon/> : <div></div>}
+                    onClick={e => build()}>
+                {isBuilding ? "..." : "Build"}
+            </Button>
+        </Tooltip>)
+    }
+
+    function deleteDeploymentButton(env: string) {
+        return (<Tooltip content="Delete deployment" position={"left"}>
+            <Button size="sm" variant="secondary"
+                    className="project-button"
+                    icon={<DeleteIcon/>}
+                    onClick={e => {
+                        setShowDeleteConfirmation(true);
+                        setDeleteEntityType("deployment");
+                        setDeleteEntityEnv(env);
+                        setDeleteEntityName(project?.projectId);
+                    }}>
+                {"Delete"}
+            </Button>
+        </Tooltip>)
+    }
+
+    function getReplicasPanel(env: string) {
+        const deploymentStatus = deployments.find(d => d.name === project?.projectId);
+        const ok = (deploymentStatus && deploymentStatus?.readyReplicas > 0
+            && (deploymentStatus.unavailableReplicas === 0 || deploymentStatus.unavailableReplicas === undefined || deploymentStatus.unavailableReplicas === null)
+            && deploymentStatus?.replicas === deploymentStatus?.readyReplicas)
+        return (
+            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
+                <FlexItem>
+                    {deploymentStatus && <LabelGroup numLabels={3}>
+                        <Tooltip content={"Ready Replicas / Replicas"} position={"left"}>
+                            <Label icon={ok ? <UpIcon/> : <DownIcon/>}
+                                   color={ok ? "green" : "grey"}>{"Replicas: " + deploymentStatus.readyReplicas + " / " + deploymentStatus.replicas}</Label>
+                        </Tooltip>
+                        {deploymentStatus.unavailableReplicas > 0 &&
+                            <Tooltip content={"Unavailable replicas"} position={"right"}>
+                                <Label icon={<DownIcon/>} color={"red"}>{deploymentStatus.unavailableReplicas}</Label>
+                            </Tooltip>
+                        }
+                    </LabelGroup>}
+                    {deploymentStatus === undefined && <Label icon={<DownIcon/>} color={"grey"}>No deployments</Label>}
+                </FlexItem>
+                <FlexItem>{env === "dev" && deleteDeploymentButton(env)}</FlexItem>
+            </Flex>
+        )
+    }
+
+    function getPipelineState(env: string) {
+        const status = pipelineStatuses.filter(p => p.projectId === project.projectId).at(0);
+        const pipeline = status?.pipelineName;
+        const pipelineResult = status?.result;
+        let lastPipelineRunTime = 0;
+        if (status?.startTime) {
+            const start: Date = new Date(status.startTime);
+            const finish: Date = status.completionTime !== undefined && status.completionTime !== null ? new Date(status.completionTime) : new Date();
+            lastPipelineRunTime = Math.round((finish.getTime() - start.getTime()) / 1000);
+        }
+        const showTime = lastPipelineRunTime && lastPipelineRunTime > 0;
+        const isRunning = pipelineResult === 'Running';
+        const isFailed = pipelineResult === 'Failed';
+        const isSucceeded = pipelineResult === 'Succeeded';
+        const color = isSucceeded ? "green" : (isFailed ? "red" : (isRunning ? "blue" : "grey"))
+        const icon = isSucceeded ? <UpIcon className="not-spinner"/> : <DownIcon className="not-spinner"/>
+        return (
+            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
+                <FlexItem>
+                    <Tooltip content={pipelineResult} position={"right"}>
+                        <LabelGroup numLabels={2}>
+                            <Label icon={isRunning ? <Spinner diameter="16px" className="spinner"/> : icon}
+                                   color={color}>
+                                {pipeline
+                                    ? <Button className='labeled-button' variant="link" onClick={e =>
+                                        useLogStore.setState({showLog: true, type: 'build', podName: pipeline})
+                                    }>
+                                        {pipeline}
+                                    </Button>
+                                    : "No builder"}
+                                {isRunning && <Tooltip content={"Stop build"}>
+                                    <Button
+                                        icon={<DeleteIcon/>}
+                                        className="labeled-button"
+                                        variant="link" onClick={e => {
+                                        setShowDeleteConfirmation(true);
+                                        setDeleteEntityType("build");
+                                        setDeleteEntityEnv(env);
+                                        setDeleteEntityName(pipeline);
+                                    }}></Button>
+                                </Tooltip>}
+                            </Label>
+                            {pipeline !== undefined && showTime === true && lastPipelineRunTime !== undefined &&
+                                <Label icon={<ClockIcon className="not-spinner"/>}
+                                       color={color}>{lastPipelineRunTime + "s"}</Label>}
+                        </LabelGroup>
+                    </Tooltip>
+                </FlexItem>
+                <FlexItem>{env === "dev" && buildButton(env)}</FlexItem>
+            </Flex>
+        )
+    }
+
+    function getBuildState(env: string) {
+        const status = containers.filter(c => c.projectId === project.projectId && c.type === 'build').at(0);
+        const buildName = status?.containerName;
+        const state = status?.state;
+        let buildTime = 0;
+        if (status?.created) {
+            const start: Date = new Date(status.created);
+            const finish: Date = status.finished !== undefined && status.finished !== null ? new Date(status.finished) : new Date();
+            buildTime = Math.round((finish.getTime() - start.getTime()) / 1000);
+        }
+        const showTime = buildTime && buildTime > 0;
+        const isRunning = state === 'running';
+        const isExited = state === 'exited';
+        const color = isExited ? "grey" : (isRunning ? "blue" : "grey");
+        const icon = isExited ? <UpIcon className="not-spinner"/> : <DownIcon className="not-spinner"/>
+        return (
+            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
+                <FlexItem>
+                    <LabelGroup numLabels={3}>
+                        <Label isEditable={!isRunning} onEditComplete={(_, v) => setTag(v)}
+                               icon={<TagIcon className="not-spinner"/>}
+                               color={color}>{tag}</Label>
+                        <Label icon={isRunning ? <Spinner diameter="16px" className="spinner"/> : icon}
+                               color={color}>
+                            {buildName
+                                ? <Button className='labeled-button' variant="link" onClick={e =>
+                                    useLogStore.setState({showLog: true, type: 'build', podName: buildName})
+                                }>
+                                    {buildName}
+                                </Button>
+                                : "No builder"}
+                            {status !== undefined && <Tooltip content={"Delete build"}>
+                                <Button
+                                    icon={<DeleteIcon/>}
+                                    className="labeled-button"
+                                    variant="link" onClick={e => {
+                                    setShowDeleteConfirmation(true);
+                                    setDeleteEntityType("build");
+                                    setDeleteEntityEnv(env);
+                                    setDeleteEntityName(buildName);
+                                }}></Button>
+                            </Tooltip>}
+                        </Label>
+                        {buildName !== undefined && showTime === true && buildTime !== undefined &&
+                            <Label icon={<ClockIcon className="not-spinner"/>}
+                                   color={color}>{buildTime + "s"}</Label>}
+                    </LabelGroup>
+                </FlexItem>
+                <FlexItem>{env === "dev" && buildButton(env)}</FlexItem>
+            </Flex>
+        )
+    }
+
+    function getDeleteConfirmation() {
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showDeleteConfirmation}
+            onClose={() => setShowDeleteConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (deleteEntityEnv && deleteEntityName && deleteEntity) {
+                        deleteEntity(deleteEntityType, deleteEntityName, deleteEntityEnv);
+                        setShowDeleteConfirmation(false);
+                    }
+                }}>Delete
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowDeleteConfirmation(false)}>
+            <div>{"Delete " + deleteEntityType + " " + deleteEntityName + "?"}</div>
+        </Modal>)
+    }
+
+    const env = props.env;
+    return (
+        <Card className="project-status">
+            <CardBody>
+                <DescriptionList isHorizontal horizontalTermWidthModifier={{default: '20ch'}}>
+                    <DescriptionListGroup>
+                        <DescriptionListTerm>Environment</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            <Badge className="badge">{env}</Badge>
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                    <DescriptionListGroup>
+                        <DescriptionListTerm>Build container with tag</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            {getBuildState(env)}
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                    {config.infrastructure === 'kubernetes' &&
+                        <DescriptionListGroup>
+                        <DescriptionListTerm>Deployment</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            {getReplicasPanel(env)}
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                    }
+                    <DescriptionListGroup>
+                        <DescriptionListTerm>Containers</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            <ContainersPanel env={props.env}/>
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                </DescriptionList>
+            </CardBody>
+            {showDeleteConfirmation && getDeleteConfirmation()}
+        </Card>
+    )
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx
new file mode 100644
index 00000000..87a43f0f
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx
@@ -0,0 +1,112 @@
+import React, {useState} from 'react';
+import {
+    Button, Tooltip, Flex, FlexItem, LabelGroup, Label, Modal
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import {KaravanApi} from "../../api/KaravanApi";
+import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
+import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
+import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon";
+import {ContainerStatus} from "../../api/ProjectModels";
+import {useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+
+interface Props {
+    env: string,
+}
+
+export function ContainersPanel (props: Props) {
+
+    const [project] = useProjectStore((s) => [s.project], shallow);
+    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
+    const [containers, deployments, camels, pipelineStatuses] =
+        useStatusesStore((s) => [s.containers, s.deployments, s.camels, s.pipelineStatuses], shallow);
+    const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
+    const [deleteEntityName, setDeleteEntityName] = useState<string>();
+    const [deleteEntityEnv, setDeleteEntityEnv] = useState<string>();
+
+    function deleteContainer(name: string, environment: string) {
+        KaravanApi.deleteContainer(environment, 'project', name, (res: any) => {
+            // if (Array.isArray(res) && Array.from(res).length > 0)
+            // onRefresh();
+        });
+    }
+
+    function deleteButton(env: string) {
+        return (<Tooltip content="Delete container" position={"left"}>
+            <Button size="sm" variant="secondary"
+                    className="project-button"
+                    icon={<DeleteIcon/>}
+                    onClick={e => {
+                        setShowDeleteConfirmation(true);
+                        setDeleteEntityEnv(env);
+                        setDeleteEntityName(project?.projectId);
+                    }}>
+                {"Delete"}
+            </Button>
+        </Tooltip>)
+    }
+
+    function getDeleteConfirmation() {
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showDeleteConfirmation}
+            onClose={() => setShowDeleteConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (deleteEntityEnv && deleteEntityName) {
+                        deleteContainer(deleteEntityName, deleteEntityEnv);
+                        setShowDeleteConfirmation(false);
+                    }
+                }}>Delete
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowDeleteConfirmation(false)}>
+            <div>{"Delete container " + deleteEntityName + "?"}</div>
+        </Modal>)
+    }
+
+    const env = props.env;
+    const conts = containers.filter(d => d.projectId === project?.projectId && d.type === 'project');
+    return (
+        <Flex justifyContent={{default: "justifyContentSpaceBetween"}}
+              alignItems={{default: "alignItemsFlexStart"}}>
+            <FlexItem>
+                {conts.length === 0 && <Label icon={<DownIcon/>} color={"grey"}>No pods</Label>}
+                <LabelGroup numLabels={2} isVertical>
+                    {conts.map((pod: ContainerStatus) => {
+                            const ready = pod.state === 'running';
+                            return (
+                                <Tooltip key={pod.containerName} content={pod.state}>
+                                    <Label icon={ready ? <UpIcon/> : <DownIcon/>} color={ready ? "green" : "red"}>
+                                        <Button variant="link" className="labeled-button"
+                                                onClick={e => {
+                                                    setShowLog(true,'container', pod.containerName);
+                                                }}>
+                                            {pod.containerName}
+                                        </Button>
+                                        <Tooltip content={"Delete Container"}>
+                                            <Button icon={<DeleteIcon/>}
+                                                    className="labeled-button"
+                                                    variant="link"
+                                                    onClick={e => {
+                                                        setShowDeleteConfirmation(true);
+                                                        setDeleteEntityEnv(env);
+                                                        setDeleteEntityName(pod.containerName);
+                                                    }}></Button>
+                                        </Tooltip>
+                                    </Label>
+                                </Tooltip>
+                            )
+                        }
+                    )}
+                </LabelGroup>
+            </FlexItem>
+            <FlexItem>{env === "dev" && deleteButton(env)}</FlexItem>
+            {showDeleteConfirmation && getDeleteConfirmation()}
+        </Flex>
+    )
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx
new file mode 100644
index 00000000..9c829267
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx
@@ -0,0 +1,222 @@
+import React, {useState} from 'react';
+import {
+    Button,
+    Tooltip,
+    Flex,
+    FlexItem,
+    Modal,
+    Panel,
+    PanelHeader,
+    TextContent,
+    Text,
+    TextVariants,
+    Bullseye, EmptyState, EmptyStateVariant, EmptyStateHeader, EmptyStateIcon, PageSection, Switch, TextInput
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import {useFilesStore, useProjectStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {Table} from "@patternfly/react-table/deprecated";
+import {Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon";
+import SetIcon from "@patternfly/react-icons/dist/esm/icons/check-icon";
+import {KaravanApi} from "../../api/KaravanApi";
+import {ProjectService} from "../../api/ProjectService";
+import {ServicesYaml} from "../../api/ServiceModels";
+import CopyIcon from "@patternfly/react-icons/dist/esm/icons/copy-icon";
+import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
+import {EventBus} from "../../designer/utils/EventBus";
+
+export function ImagesPanel () {
+
+    const [project, images] = useProjectStore((s) => [s.project, s.images], shallow);
+    const [files] = useFilesStore((s) => [s.files], shallow);
+    const [showSetConfirmation, setShowSetConfirmation] = useState<boolean>(false);
+    const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
+    const [imageName, setImageName] = useState<string>();
+    const [commitChanges, setCommitChanges] = useState<boolean>(false);
+    const [commitMessage, setCommitMessage] = useState('');
+
+    function setProjectImage() {
+        if (imageName) {
+            KaravanApi.setProjectImage(project.projectId, imageName, commitChanges, commitMessage, (res: any) => {
+                ProjectService.refreshProjectData(project.projectId);
+            });
+        }
+    }
+
+    function getProjectImage(): string | undefined {
+        const file = files.filter(f => f.name === 'project-compose.yaml').at(0);
+        if (file) {
+            const dc = ServicesYaml.yamlToServices(file.code);
+            const dcs = dc.services.filter(s => s.container_name === project.projectId).at(0);
+            return dcs?.image;
+        }
+        return undefined;
+    }
+
+    function getSetConfirmation() {
+        const index = imageName?.lastIndexOf(":");
+        const name = imageName?.substring(0, index);
+        const tag = index ? imageName?.substring(index+1) : "";
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showSetConfirmation}
+            onClose={() => setShowSetConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (imageName) {
+                        setProjectImage();
+                        setShowSetConfirmation(false);
+                        setCommitChanges(false);
+                    }
+                }}>Set
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => {
+                            setShowSetConfirmation(false);
+                            setCommitChanges(false);
+                        }}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowSetConfirmation(false)}>
+            <Flex direction={{default:"column"}} justifyContent={{default:"justifyContentFlexStart"}}>
+                <FlexItem>
+                    <div>{"Set image for project " + project.projectId + ":"}</div>
+                    <div>{"Name: " + name}</div>
+                    <div>{"Tag: " + tag}</div>
+                </FlexItem>
+                <FlexItem>
+                    <Switch
+                        id="commit-switch"
+                        label="Commit changes"
+                        isChecked={commitChanges}
+                        onChange={(event, checked) => setCommitChanges(checked)}
+                        isReversed
+                    />
+                </FlexItem>
+                {commitChanges && <FlexItem>
+                    <TextInput value={commitMessage} type="text"
+                               onChange={(_, value) => setCommitMessage(value)}
+                               aria-label="commit message"/>
+                </FlexItem>}
+            </Flex>
+        </Modal>)
+    }
+
+    function getDeleteConfirmation() {
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showDeleteConfirmation}
+            onClose={() => setShowDeleteConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (imageName) {
+                        KaravanApi.deleteImage(imageName, () => {
+                            EventBus.sendAlert("Image deleted", "Image " + imageName + " deleted", 'info');
+                            setShowDeleteConfirmation(false);
+                        });
+                    }
+                }}>Delete
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowDeleteConfirmation(false)}>
+            <div>{"Delete image:"}</div>
+            <div>{imageName}</div>
+        </Modal>)
+    }
+
+    const projectImage = getProjectImage();
+    return (
+        <PageSection className="project-tab-panel project-images-panel" padding={{default: "padding"}}>
+            <Panel>
+                <PanelHeader>
+                    <Flex direction={{default: "row"}} justifyContent={{default:"justifyContentFlexStart"}}>
+                        <FlexItem>
+                            <TextContent>
+                                <Text component={TextVariants.h6}>Images</Text>
+                            </TextContent>
+                        </FlexItem>
+                        <FlexItem>
+
+                        </FlexItem>
+                    </Flex>
+                </PanelHeader>
+            </Panel>
+            <Table aria-label="Images" variant={"compact"} className={"table"}>
+                <Thead>
+                    <Tr>
+                        <Th key='status' width={10}></Th>
+                        <Th key='image' width={30}>Image</Th>
+                        <Th key='tag' width={10}>Tag</Th>
+                        <Th key='actions'></Th>
+                    </Tr>
+                </Thead>
+                <Tbody>
+                    {images.map(image => {
+                        const index = image.lastIndexOf(":");
+                        const name = image.substring(0, index);
+                        const tag = image.substring(index+1);
+                        return <Tr key={image}>
+                            <Td modifier={"fitContent"} >
+                                {image === projectImage ? <SetIcon/> : <div/>}
+                            </Td>
+                            <Td>
+                                {name}
+                            </Td>
+                            <Td>
+                                {tag}
+                            </Td>
+                            <Td modifier={"fitContent"} isActionCell>
+                                <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexEnd"}}
+                                      spaceItems={{default: 'spaceItemsNone'}}>
+                                    <FlexItem>
+                                        <Tooltip content={"Delete image"} position={"bottom"}>
+                                            <Button variant={"plain"}
+                                                    icon={<DeleteIcon/>}
+                                                    isDisabled={image === projectImage}
+                                                    onClick={e => {
+                                                        setImageName(image);
+                                                        setShowDeleteConfirmation(true);
+                                                    }}>
+                                            </Button>
+                                        </Tooltip>
+                                    </FlexItem>
+                                    <FlexItem>
+                                        <Tooltip content="Set project image" position={"bottom"}>
+                                            <Button style={{padding: '0'}}
+                                                    variant={"plain"}
+                                                    isDisabled={image === projectImage}
+                                                    onClick={e => {
+                                                        setImageName(image);
+                                                        setCommitMessage(commitMessage === '' ? new Date().toLocaleString() : commitMessage);
+                                                        setShowSetConfirmation(true);
+                                                    }}>
+                                                <SetIcon/>
+                                            </Button>
+                                        </Tooltip>
+                                    </FlexItem>
+                                </Flex>
+                            </Td>
+                        </Tr>
+                    })}
+                    {images.length === 0 &&
+                        <Tr>
+                            <Td colSpan={8}>
+                                <Bullseye>
+                                    <EmptyState variant={EmptyStateVariant.sm}>
+                                        <EmptyStateHeader titleText="No results found" icon={<EmptyStateIcon icon={SearchIcon}/>} headingLevel="h2" />
+                                    </EmptyState>
+                                </Bullseye>
+                            </Td>
+                        </Tr>
+                    }
+                </Tbody>
+            </Table>
+            {showSetConfirmation && getSetConfirmation()}
+            {showDeleteConfirmation && getDeleteConfirmation()}
+        </PageSection>
+    )
+}


[camel-karavan] 04/05: Kubernetesless forst full process for #817

Posted by ma...@apache.org.
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 482c50f108928c6c1c078886f2ec0b1c3c0637d3
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Sep 8 19:45:53 2023 -0400

    Kubernetesless forst full process for #817
---
 .../src/main/webui/src/project/ProjectPanel.tsx    |   4 +
 .../src/main/webui/src/project/ProjectToolbar.tsx  |   8 +-
 .../main/webui/src/project/build/BuildPanel.tsx    | 100 +++++++-----------
 .../webui/src/project/build/ContainersPanel.tsx    | 112 ---------------------
 .../webui/src/project/build/ProjectBuildTab.tsx    |   9 +-
 .../ContainerButtons.tsx}                          |  95 ++++++++++-------
 .../webui/src/project/container/ContainerPanel.tsx |  90 +++++++++++++++++
 .../ProjectContainerTab.tsx}                       |  14 +--
 8 files changed, 202 insertions(+), 230 deletions(-)

diff --git a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
index 27246306..6838dbf1 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx
@@ -12,6 +12,8 @@ import {ProjectBuildTab} from "./build/ProjectBuildTab";
 import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
 import {ImagesPanel} from "./build/ImagesPanel";
+import {ContainerButtons} from "./container/ContainerButtons";
+import {ProjectContainerTab} from "./container/ProjectContainerTab";
 
 export function ProjectPanel () {
 
@@ -40,6 +42,7 @@ export function ProjectPanel () {
                     <Tab eventKey="dashboard" title="Dashboard"/>
                     <Tab eventKey="trace" title="Trace"/>
                     <Tab eventKey="build" title="Build"/>
+                    <Tab eventKey="container" title="Container"/>
                 </Tabs>}
             </FlexItem>
             <FlexItem>
@@ -51,6 +54,7 @@ export function ProjectPanel () {
                         {tab === 'trace' && project && <TraceTab/>}
                         {tab === 'build' && <ProjectBuildTab/>}
                         {tab === 'build' && <ImagesPanel/>}
+                        {tab === 'container' && <ProjectContainerTab/>}
                     </>
                 }
             </FlexItem>
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx b/karavan-web/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
index 0dd876cc..84f0cd2c 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
@@ -109,7 +109,7 @@ export function ProjectToolbar () {
         return (<Toolbar id="toolbar-group-types">
             <ToolbarContent>
                 {isRunnable() && <DevModeToolbar/>}
-                {isBuild() && <BuildToolbar/>}
+                {isBuildContainer() && <BuildToolbar/>}
             </ToolbarContent>
         </Toolbar>)
     }
@@ -127,11 +127,11 @@ export function ProjectToolbar () {
     }
 
     function isRunnable(): boolean {
-        return !isKameletsProject() && !isTemplatesProject() && !isServicesProject() && tabIndex !== 'build';
+        return !isKameletsProject() && !isTemplatesProject() && !isServicesProject() && !['build', 'container'].includes(tabIndex.toString());
     }
 
-    function isBuild(): boolean {
-        return !isKameletsProject() && !isTemplatesProject() && !isServicesProject() && tabIndex === 'build';
+    function isBuildContainer(): boolean {
+        return !isKameletsProject() && !isTemplatesProject() && !isServicesProject() && ['build', 'container'].includes(tabIndex.toString());
     }
 
     function allowAddFiles(): boolean {
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx
index 752f496c..eb4efa03 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx
@@ -16,13 +16,9 @@ import TagIcon from "@patternfly/react-icons/dist/esm/icons/tag-icon";
 import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon";
 import {useAppConfigStore, useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
 import {shallow} from "zustand/shallow";
-import {ContainersPanel} from "./ContainersPanel";
+import {EventBus} from "../../designer/utils/EventBus";
 
-interface Props {
-    env: string,
-}
-
-export function BuildPanel (props: Props) {
+export function BuildPanel () {
 
     const [config] = useAppConfigStore((state) => [state.config], shallow)
     const [project] = useProjectStore((s) => [s.project], shallow);
@@ -32,31 +28,18 @@ export function BuildPanel (props: Props) {
     const [isPushing, setIsPushing] = useState<boolean>(false);
     const [isBuilding, setIsBuilding] = useState<boolean>(false);
     const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
-    const [deleteEntityType, setDeleteEntityType] = useState<'pod' | 'deployment' | 'build'>('pod');
     const [deleteEntityName, setDeleteEntityName] = useState<string>();
-    const [deleteEntityEnv, setDeleteEntityEnv] = useState<string>();
-    const [tag, setTag] = useState<string>(new Date().toISOString().substring(0,19).replaceAll(':', '-'));
+    const [tag, setTag] = useState<string>(
+        new Date().toISOString().substring(0,19).replaceAll(':', '').replaceAll('-', '')
+    );
 
-    function deleteEntity(type: 'pod' | 'deployment' | 'build', name: string, environment: string) {
-        switch (type) {
-            case "deployment":
-                KaravanApi.deleteDeployment(environment, name, (res: any) => {
-                    // if (Array.isArray(res) && Array.from(res).length > 0)
-                    // onRefresh();
-                });
-                break;
-            case "pod":
-                KaravanApi.deleteContainer(environment, 'project', name, (res: any) => {
-                    // if (Array.isArray(res) && Array.from(res).length > 0)
-                    // onRefresh();
-                });
-                break;
-            case "build":
-                KaravanApi.stopBuild(environment, name, (res: any) => {
-                    // if (Array.isArray(res) && Array.from(res).length > 0)
-                    // onRefresh();
-                });
-                break;
+    function deleteEntity() {
+        if (deleteEntityName) {
+            KaravanApi.stopBuild('dev', deleteEntityName, (res: any) => {
+                if (res.status === 200) {
+                    EventBus.sendAlert("Build deleted", "Build deleted: " + deleteEntityName, 'info');
+                }
+            });
         }
     }
 
@@ -72,7 +55,7 @@ export function BuildPanel (props: Props) {
         });
     }
 
-    function buildButton(env: string) {
+    function buildButton() {
         const status = pipelineStatuses.filter(p => p.projectId === project.projectId).at(0);
         const isRunning = status?.result === 'Running';
         return (<Tooltip content="Start build" position={"left"}>
@@ -95,8 +78,6 @@ export function BuildPanel (props: Props) {
                     icon={<DeleteIcon/>}
                     onClick={e => {
                         setShowDeleteConfirmation(true);
-                        setDeleteEntityType("deployment");
-                        setDeleteEntityEnv(env);
                         setDeleteEntityName(project?.projectId);
                     }}>
                 {"Delete"}
@@ -166,8 +147,6 @@ export function BuildPanel (props: Props) {
                                         className="labeled-button"
                                         variant="link" onClick={e => {
                                         setShowDeleteConfirmation(true);
-                                        setDeleteEntityType("build");
-                                        setDeleteEntityEnv(env);
                                         setDeleteEntityName(pipeline);
                                     }}></Button>
                                 </Tooltip>}
@@ -178,12 +157,12 @@ export function BuildPanel (props: Props) {
                         </LabelGroup>
                     </Tooltip>
                 </FlexItem>
-                <FlexItem>{env === "dev" && buildButton(env)}</FlexItem>
+                <FlexItem>{buildButton()}</FlexItem>
             </Flex>
         )
     }
 
-    function getBuildState(env: string) {
+    function getBuildState() {
         const status = containers.filter(c => c.projectId === project.projectId && c.type === 'build').at(0);
         const buildName = status?.containerName;
         const state = status?.state;
@@ -202,9 +181,6 @@ export function BuildPanel (props: Props) {
             <Flex justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
                 <FlexItem>
                     <LabelGroup numLabels={3}>
-                        <Label isEditable={!isRunning} onEditComplete={(_, v) => setTag(v)}
-                               icon={<TagIcon className="not-spinner"/>}
-                               color={color}>{tag}</Label>
                         <Label icon={isRunning ? <Spinner diameter="16px" className="spinner"/> : icon}
                                color={color}>
                             {buildName
@@ -220,8 +196,6 @@ export function BuildPanel (props: Props) {
                                     className="labeled-button"
                                     variant="link" onClick={e => {
                                     setShowDeleteConfirmation(true);
-                                    setDeleteEntityType("build");
-                                    setDeleteEntityEnv(env);
                                     setDeleteEntityName(buildName);
                                 }}></Button>
                             </Tooltip>}
@@ -231,11 +205,24 @@ export function BuildPanel (props: Props) {
                                    color={color}>{buildTime + "s"}</Label>}
                     </LabelGroup>
                 </FlexItem>
-                <FlexItem>{env === "dev" && buildButton(env)}</FlexItem>
+                <FlexItem>{buildButton()}</FlexItem>
             </Flex>
         )
     }
 
+    function getBuildTag() {
+        const status = containers.filter(c => c.projectId === project.projectId && c.type === 'build').at(0);
+        const state = status?.state;
+        const isRunning = state === 'running';
+        const isExited = state === 'exited';
+        const color = isExited ? "grey" : (isRunning ? "blue" : "grey");
+        return (
+            <Label isEditable={!isRunning} onEditComplete={(_, v) => setTag(v)}
+                   icon={<TagIcon className="not-spinner"/>}
+                   color={color}>{tag}</Label>
+        )
+    }
+
     function getDeleteConfirmation() {
         return (<Modal
             className="modal-delete"
@@ -244,8 +231,8 @@ export function BuildPanel (props: Props) {
             onClose={() => setShowDeleteConfirmation(false)}
             actions={[
                 <Button key="confirm" variant="primary" onClick={e => {
-                    if (deleteEntityEnv && deleteEntityName && deleteEntity) {
-                        deleteEntity(deleteEntityType, deleteEntityName, deleteEntityEnv);
+                    if (deleteEntityName && deleteEntity) {
+                        deleteEntity();
                         setShowDeleteConfirmation(false);
                     }
                 }}>Delete
@@ -254,39 +241,24 @@ export function BuildPanel (props: Props) {
                         onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button>
             ]}
             onEscapePress={e => setShowDeleteConfirmation(false)}>
-            <div>{"Delete " + deleteEntityType + " " + deleteEntityName + "?"}</div>
+            <div>{"Delete build " + deleteEntityName + "?"}</div>
         </Modal>)
     }
 
-    const env = props.env;
     return (
         <Card className="project-status">
             <CardBody>
                 <DescriptionList isHorizontal horizontalTermWidthModifier={{default: '20ch'}}>
                     <DescriptionListGroup>
-                        <DescriptionListTerm>Environment</DescriptionListTerm>
-                        <DescriptionListDescription>
-                            <Badge className="badge">{env}</Badge>
-                        </DescriptionListDescription>
-                    </DescriptionListGroup>
-                    <DescriptionListGroup>
-                        <DescriptionListTerm>Build container with tag</DescriptionListTerm>
+                        <DescriptionListTerm>Tag</DescriptionListTerm>
                         <DescriptionListDescription>
-                            {getBuildState(env)}
+                            {getBuildTag()}
                         </DescriptionListDescription>
                     </DescriptionListGroup>
-                    {config.infrastructure === 'kubernetes' &&
-                        <DescriptionListGroup>
-                        <DescriptionListTerm>Deployment</DescriptionListTerm>
-                        <DescriptionListDescription>
-                            {getReplicasPanel(env)}
-                        </DescriptionListDescription>
-                    </DescriptionListGroup>
-                    }
                     <DescriptionListGroup>
-                        <DescriptionListTerm>Containers</DescriptionListTerm>
+                        <DescriptionListTerm>Build container</DescriptionListTerm>
                         <DescriptionListDescription>
-                            <ContainersPanel env={props.env}/>
+                            {getBuildState()}
                         </DescriptionListDescription>
                     </DescriptionListGroup>
                 </DescriptionList>
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx
deleted file mode 100644
index 87a43f0f..00000000
--- a/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import React, {useState} from 'react';
-import {
-    Button, Tooltip, Flex, FlexItem, LabelGroup, Label, Modal
-} from '@patternfly/react-core';
-import '../../designer/karavan.css';
-import {KaravanApi} from "../../api/KaravanApi";
-import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
-import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
-import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon";
-import {ContainerStatus} from "../../api/ProjectModels";
-import {useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
-import {shallow} from "zustand/shallow";
-
-interface Props {
-    env: string,
-}
-
-export function ContainersPanel (props: Props) {
-
-    const [project] = useProjectStore((s) => [s.project], shallow);
-    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
-    const [containers, deployments, camels, pipelineStatuses] =
-        useStatusesStore((s) => [s.containers, s.deployments, s.camels, s.pipelineStatuses], shallow);
-    const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
-    const [deleteEntityName, setDeleteEntityName] = useState<string>();
-    const [deleteEntityEnv, setDeleteEntityEnv] = useState<string>();
-
-    function deleteContainer(name: string, environment: string) {
-        KaravanApi.deleteContainer(environment, 'project', name, (res: any) => {
-            // if (Array.isArray(res) && Array.from(res).length > 0)
-            // onRefresh();
-        });
-    }
-
-    function deleteButton(env: string) {
-        return (<Tooltip content="Delete container" position={"left"}>
-            <Button size="sm" variant="secondary"
-                    className="project-button"
-                    icon={<DeleteIcon/>}
-                    onClick={e => {
-                        setShowDeleteConfirmation(true);
-                        setDeleteEntityEnv(env);
-                        setDeleteEntityName(project?.projectId);
-                    }}>
-                {"Delete"}
-            </Button>
-        </Tooltip>)
-    }
-
-    function getDeleteConfirmation() {
-        return (<Modal
-            className="modal-delete"
-            title="Confirmation"
-            isOpen={showDeleteConfirmation}
-            onClose={() => setShowDeleteConfirmation(false)}
-            actions={[
-                <Button key="confirm" variant="primary" onClick={e => {
-                    if (deleteEntityEnv && deleteEntityName) {
-                        deleteContainer(deleteEntityName, deleteEntityEnv);
-                        setShowDeleteConfirmation(false);
-                    }
-                }}>Delete
-                </Button>,
-                <Button key="cancel" variant="link"
-                        onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button>
-            ]}
-            onEscapePress={e => setShowDeleteConfirmation(false)}>
-            <div>{"Delete container " + deleteEntityName + "?"}</div>
-        </Modal>)
-    }
-
-    const env = props.env;
-    const conts = containers.filter(d => d.projectId === project?.projectId && d.type === 'project');
-    return (
-        <Flex justifyContent={{default: "justifyContentSpaceBetween"}}
-              alignItems={{default: "alignItemsFlexStart"}}>
-            <FlexItem>
-                {conts.length === 0 && <Label icon={<DownIcon/>} color={"grey"}>No pods</Label>}
-                <LabelGroup numLabels={2} isVertical>
-                    {conts.map((pod: ContainerStatus) => {
-                            const ready = pod.state === 'running';
-                            return (
-                                <Tooltip key={pod.containerName} content={pod.state}>
-                                    <Label icon={ready ? <UpIcon/> : <DownIcon/>} color={ready ? "green" : "red"}>
-                                        <Button variant="link" className="labeled-button"
-                                                onClick={e => {
-                                                    setShowLog(true,'container', pod.containerName);
-                                                }}>
-                                            {pod.containerName}
-                                        </Button>
-                                        <Tooltip content={"Delete Container"}>
-                                            <Button icon={<DeleteIcon/>}
-                                                    className="labeled-button"
-                                                    variant="link"
-                                                    onClick={e => {
-                                                        setShowDeleteConfirmation(true);
-                                                        setDeleteEntityEnv(env);
-                                                        setDeleteEntityName(pod.containerName);
-                                                    }}></Button>
-                                        </Tooltip>
-                                    </Label>
-                                </Tooltip>
-                            )
-                        }
-                    )}
-                </LabelGroup>
-            </FlexItem>
-            <FlexItem>{env === "dev" && deleteButton(env)}</FlexItem>
-            {showDeleteConfirmation && getDeleteConfirmation()}
-        </Flex>
-    )
-}
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
index e761bbcb..7c980820 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
@@ -2,20 +2,13 @@ import React from 'react';
 import '../../designer/karavan.css';
 import {BuildPanel} from "./BuildPanel";
 import {PageSection} from "@patternfly/react-core";
-import {useAppConfigStore, useProjectStore} from "../../api/ProjectStore";
 
 export function ProjectBuildTab () {
 
-    const {config} = useAppConfigStore();
-    const {project} = useProjectStore();
-
     return (
         <PageSection className="project-tab-panel project-build-panel" padding={{default: "padding"}}>
             <div>
-                {/*{["dev", "test", "prod"].map(env =>*/}
-                {config.environments.map(env =>
-                    <BuildPanel key={env} env={env}/>
-                )}
+                <BuildPanel/>
             </div>
         </PageSection>
     )
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/ContainerPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/container/ContainerButtons.tsx
similarity index 53%
rename from karavan-web/karavan-app/src/main/webui/src/project/ContainerPanel.tsx
rename to karavan-web/karavan-app/src/main/webui/src/project/container/ContainerButtons.tsx
index c32fb4a7..1fafb4d8 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/ContainerPanel.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/container/ContainerButtons.tsx
@@ -1,24 +1,19 @@
-import React from 'react';
-import {Button, Flex, FlexItem, Label, Spinner, Tooltip, TooltipPosition} from '@patternfly/react-core';
-import '../designer/karavan.css';
+import React, {useState} from 'react';
+import {Button, Flex, FlexItem, Modal, Spinner, Tooltip, TooltipPosition} from '@patternfly/react-core';
+import '../../designer/karavan.css';
 import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon";
-import {useAppConfigStore, useDevModeStore, useLogStore, useProjectStore, useStatusesStore} from "../api/ProjectStore";
+import {useAppConfigStore, useDevModeStore, useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
 import {shallow} from "zustand/shallow";
-import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
-import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
 import RunIcon from "@patternfly/react-icons/dist/esm/icons/play-icon";
-import {ProjectService} from "../api/ProjectService";
-import ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
-import {KaravanApi} from "../api/KaravanApi";
-import {ProjectEventBus} from "../api/ProjectEventBus";
-import {EventBus} from "../designer/utils/EventBus";
+import {KaravanApi} from "../../api/KaravanApi";
 import StopIcon from "@patternfly/react-icons/dist/js/icons/stop-icon";
 
+
 interface Props {
-    reloadOnly?: boolean
+    env: string,
 }
 
-export function ContainerPanel (props: Props) {
+export function ContainerButtons (props: Props) {
 
     const [config] = useAppConfigStore((state) => [state.config], shallow)
     const [status] = useDevModeStore((state) => [state.status], shallow)
@@ -26,29 +21,61 @@ export function ContainerPanel (props: Props) {
     const [containers] = useStatusesStore((state) => [state.containers], shallow);
     const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
 
+    const [showConfirmation, setShowConfirmation] = useState<boolean>(false);
+    const [actionType, setActionType] = useState<'run' | 'stop' | 'delete'>('run');
+
     const containerStatus = containers.filter(c => c.containerName === project.projectId).at(0);
     const commands = containerStatus?.commands || ['run'];
     const isRunning = containerStatus?.state === 'running';
     const inTransit = containerStatus?.inTransit;
     const isLoading = status === 'wip';
-    const color = containerStatus?.state === 'running' ? "green" : "grey";
-    const icon = isRunning ? <UpIcon/> : <DownIcon/>;
+
+    function act() {
+        switch (actionType) {
+            case "run":
+                KaravanApi.manageContainer(props.env, 'project', project.projectId, 'run', res => {
+                    setShowLog(false, 'container', undefined)
+                });
+                break;
+            case "stop":
+                KaravanApi.manageContainer(props.env, 'project', project.projectId, 'stop', res => {
+                    setShowLog(false, 'container', undefined)
+                });
+                break;
+            case "delete":
+                KaravanApi.manageContainer(props.env, 'project', project.projectId, 'delete', res => {
+                    setShowLog(false, 'container', undefined)
+                });
+                break;
+        }
+    }
+
+    function getDeleteConfirmation() {
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showConfirmation}
+            onClose={() => setShowConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (actionType && project.projectId) {
+                        act();
+                        setShowConfirmation(false);
+                    }
+                }}>Delete
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => setShowConfirmation(false)}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowConfirmation(false)}>
+            <div>{"Confirm " + actionType + " container?"}</div>
+        </Modal>)
+    }
 
     return (<Flex className="toolbar" direction={{default: "row"}} alignItems={{default: "alignItemsCenter"}}>
         <FlexItem>
             {(inTransit || isLoading) && <Spinner size="lg" aria-label="spinner"/>}
         </FlexItem>
-        {containerStatus?.containerId && <FlexItem>
-            <Label icon={icon} color={color}>
-                <Tooltip content={"Show log"} position={TooltipPosition.bottom}>
-                    <Button className='labeled-button' variant="link" isDisabled={!isRunning}
-                            onClick={e =>
-                                setShowLog(true, 'container', containerStatus.containerName)}>
-                        {containerStatus.containerName}
-                    </Button>
-                </Tooltip>
-            </Label>
-        </FlexItem>}
         {!isRunning && <FlexItem>
             <Tooltip content="Run container" position={TooltipPosition.bottom}>
                 <Button size="sm"
@@ -56,9 +83,8 @@ export function ContainerPanel (props: Props) {
                         variant={"primary"}
                         icon={<RunIcon/>}
                         onClick={() => {
-                            KaravanApi.manageContainer('dev', 'project', project.projectId, 'run', res => {
-                                setShowLog(false, 'container', undefined)
-                            });
+                            setActionType('run');
+                            setShowConfirmation(true);
                         }}>
                     {"Run"}
                 </Button>
@@ -72,9 +98,8 @@ export function ContainerPanel (props: Props) {
                             variant={"control"}
                             icon={<StopIcon/>}
                             onClick={() => {
-                                KaravanApi.manageContainer('dev', 'project', project.projectId, 'stop', res => {
-                                    setShowLog(false, 'container', undefined)
-                                });
+                                setActionType('stop');
+                                setShowConfirmation(true);
                             }}>
                     </Button>
                 </Tooltip>
@@ -87,12 +112,12 @@ export function ContainerPanel (props: Props) {
                         variant={"control"}
                         icon={<DeleteIcon/>}
                         onClick={() => {
-                            KaravanApi.manageContainer('dev', 'project', project.projectId, 'delete', res => {
-                                setShowLog(false, 'container', undefined)
-                            });
+                            setActionType('delete');
+                            setShowConfirmation(true);
                         }}>
                 </Button>
             </Tooltip>
         </FlexItem>
+        {showConfirmation && getDeleteConfirmation()}
     </Flex>);
 }
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/container/ContainerPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/container/ContainerPanel.tsx
new file mode 100644
index 00000000..f1bda323
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/container/ContainerPanel.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import {
+    Badge,
+    Button,
+    Card,
+    CardBody,
+    DescriptionList,
+    DescriptionListDescription,
+    DescriptionListGroup,
+    DescriptionListTerm,
+    Flex,
+    FlexItem,
+    Label,
+    LabelGroup,
+    Tooltip,
+    TooltipPosition
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import UpIcon from "@patternfly/react-icons/dist/esm/icons/running-icon";
+import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
+import {useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {ContainerStatus} from "../../api/ProjectModels";
+import {ContainerButtons} from "./ContainerButtons";
+
+interface Props {
+    env: string,
+}
+
+export function ContainerPanel (props: Props) {
+
+    const [project] = useProjectStore((s) => [s.project], shallow);
+    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
+    const [containers, deployments, camels, pipelineStatuses] =
+        useStatusesStore((s) => [s.containers, s.deployments, s.camels, s.pipelineStatuses], shallow);
+
+    function getButtons() {
+        const env = props.env;
+        const conts = containers.filter(d => d.projectId === project?.projectId && d.type === 'project');
+        return (
+            <Flex justifyContent={{default: "justifyContentSpaceBetween"}}
+                  alignItems={{default: "alignItemsFlexStart"}}>
+                <FlexItem>
+                    {conts.length === 0 && <Label icon={<DownIcon/>} color={"grey"}>No pods</Label>}
+                    <LabelGroup numLabels={2} isVertical>
+                        {conts.map((pod: ContainerStatus) => {
+                                const ready = pod.state === 'running';
+                                return (
+                                    <Tooltip key={pod.containerName} content={pod.state} position={TooltipPosition.left}>
+                                        <Label icon={ready ? <UpIcon/> : <DownIcon/>} color={ready ? "green" : "grey"}>
+                                            <Button variant="link" className="labeled-button"
+                                                    onClick={e => {
+                                                        setShowLog(true, 'container', pod.containerName);
+                                                    }}>
+                                                {pod.containerName}
+                                            </Button>
+                                        </Label>
+                                    </Tooltip>
+                                )
+                            }
+                        )}
+                    </LabelGroup>
+                </FlexItem>
+                <FlexItem>{env === "dev" && <ContainerButtons env={env}/>}</FlexItem>
+            </Flex>
+        )
+    }
+
+    const env = props.env;
+    return (
+        <Card className="project-status">
+            <CardBody>
+                <DescriptionList isHorizontal horizontalTermWidthModifier={{default: '20ch'}}>
+                    <DescriptionListGroup>
+                        <DescriptionListTerm>Environment</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            <Badge className="badge">{env}</Badge>
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                    <DescriptionListGroup>
+                        <DescriptionListTerm>Containers</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            {getButtons()}
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                </DescriptionList>
+            </CardBody>
+        </Card>
+    )
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx b/karavan-web/karavan-app/src/main/webui/src/project/container/ProjectContainerTab.tsx
similarity index 50%
copy from karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
copy to karavan-web/karavan-app/src/main/webui/src/project/container/ProjectContainerTab.tsx
index e761bbcb..08eb587a 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/container/ProjectContainerTab.tsx
@@ -1,20 +1,20 @@
 import React from 'react';
+import {
+    PageSection
+} from '@patternfly/react-core';
 import '../../designer/karavan.css';
-import {BuildPanel} from "./BuildPanel";
-import {PageSection} from "@patternfly/react-core";
-import {useAppConfigStore, useProjectStore} from "../../api/ProjectStore";
+import {useAppConfigStore} from "../../api/ProjectStore";
+import {ContainerPanel} from "./ContainerPanel";
 
-export function ProjectBuildTab () {
+export function ProjectContainerTab() {
 
     const {config} = useAppConfigStore();
-    const {project} = useProjectStore();
 
     return (
         <PageSection className="project-tab-panel project-build-panel" padding={{default: "padding"}}>
             <div>
-                {/*{["dev", "test", "prod"].map(env =>*/}
                 {config.environments.map(env =>
-                    <BuildPanel key={env} env={env}/>
+                    <ContainerPanel key={env} env={env}/>
                 )}
             </div>
         </PageSection>


[camel-karavan] 03/05: DevMode vs Run prod for #817

Posted by ma...@apache.org.
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 624691aa8100782b1204b43e321202febe0ba7ef
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Sep 8 19:06:46 2023 -0400

    DevMode vs Run prod for #817
---
 .../apache/camel/karavan/api/ImagesResource.java   |  1 -
 .../camel/karavan/docker/DockerForKaravan.java     |  8 +--
 .../camel/karavan/service/ProjectService.java      |  5 +-
 .../src/main/webui/src/api/KaravanApi.tsx          |  5 +-
 .../src/main/webui/src/api/ProjectModels.ts        |  2 +-
 .../src/main/webui/src/api/ProjectService.ts       |  4 +-
 .../src/main/webui/src/project/BuildToolbar.tsx    | 67 +++-------------------
 .../{BuildToolbar.tsx => ContainerPanel.tsx}       |  2 +-
 .../src/main/webui/src/project/DevModeToolbar.tsx  | 33 +++++------
 .../main/webui/src/project/build/ImagesPanel.tsx   |  4 +-
 10 files changed, 38 insertions(+), 93 deletions(-)

diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
index b4605110..0c172856 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
@@ -75,7 +75,6 @@ public class ImagesResource {
     @Path("/{imageName}")
     public Response deleteImage(@HeaderParam("username") String username, @PathParam("imageName") String imageName) {
         imageName= new String(Base64.decode(imageName));
-        System.out.println(imageName);
         if (ConfigService.inKubernetes()) {
             return Response.ok().build();
         } else {
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
index 41861af7..b551d547 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
@@ -45,13 +45,13 @@ public class DockerForKaravan {
     @Inject
     RegistryService registryService;
 
-    public void runProjectInDevMode(String projectId, String jBangOptions, Map<Integer, Integer> ports, Map<String, String> files) throws Exception {
-        Container c = createDevmodeContainer(projectId, jBangOptions, ports);
+    public void runProjectInDevMode(String projectId, String jBangOptions, Map<Integer, Integer> ports, Map<String, String> files, Map<String, String> volumes) throws Exception {
+        Container c = createDevmodeContainer(projectId, jBangOptions, ports, volumes);
         dockerService.runContainer(projectId);
         dockerService.copyFiles(c.getId(), "/code", files);
     }
 
-    protected Container createDevmodeContainer(String projectId, String jBangOptions, Map<Integer, Integer> ports) throws InterruptedException {
+    protected Container createDevmodeContainer(String projectId, String jBangOptions, Map<Integer, Integer> ports, Map<String, String> volumes) throws InterruptedException {
         LOGGER.infof("DevMode starting for %s with JBANG_OPTIONS=%s", projectId, jBangOptions);
 
         HealthCheck healthCheck = new HealthCheck().withTest(List.of("CMD", "curl", "-f", "http://localhost:8080/q/dev/health"))
@@ -64,7 +64,7 @@ public class DockerForKaravan {
         return dockerService.createContainer(projectId, devmodeImage,
                 env, ports, healthCheck,
                 Map.of(LABEL_TYPE, ContainerStatus.ContainerType.devmode.name(), LABEL_PROJECT_ID, projectId),
-                Map.of(), null);
+                volumes, null);
 
     }
 
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
index ebd49e08..b92d8e23 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
@@ -113,7 +113,10 @@ public class ProjectService implements HealthCheck {
                         .collect(Collectors.toMap(ProjectFile::getName, ProjectFile::getCode));
                 ProjectFile compose = infinispanService.getProjectFile(project.getProjectId(), PROJECT_COMPOSE_FILENAME);
                 DockerComposeService dcs = DockerComposeConverter.fromCode(compose.getCode(), project.getProjectId());
-                dockerForKaravan.runProjectInDevMode(project.getProjectId(), jBangOptions, dcs.getPortsMap(), files);
+                Map<String, String> volumes = mavenCache
+                        .map(s -> Map.of(s, "/root/.m2"))
+                        .orElseGet(Map::of);
+                dockerForKaravan.runProjectInDevMode(project.getProjectId(), jBangOptions, dcs.getPortsMap(), files, volumes);
             }
             return containerName;
         } else {
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 63573e24..05915131 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
@@ -485,7 +485,7 @@ export class KaravanApi {
     }
 
     static async manageContainer(environment: string,
-                                 type: 'devmove' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown',
+                                 type: 'devmode' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown',
                                  name: string,
                                  command: 'run' | 'pause' | 'stop' | 'delete',
                                  after: (res: AxiosResponse<any>) => void) {
@@ -497,7 +497,7 @@ export class KaravanApi {
         });
     }
 
-    static async deleteContainer(environment: string, type: 'devmove' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown', name: string, after: (res: AxiosResponse<any>) => void) {
+    static async deleteContainer(environment: string, type: 'devmode' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown', name: string, after: (res: AxiosResponse<any>) => void) {
         instance.delete('/api/container/' + environment + '/' + type + "/" + name)
             .then(res => {
                 after(res);
@@ -531,7 +531,6 @@ export class KaravanApi {
     static async deleteImage(imageName: string, after: () => void) {
         instance.delete('/api/image/' + Buffer.from(imageName).toString('base64'))
             .then(res => {
-                console.log(res.status)
                 if (res.status === 200) {
                     after();
                 }
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
index 8dd5e485..6ca10133 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
@@ -73,7 +73,7 @@ export class ContainerStatus {
     deployment: string = '';
     projectId: string = '';
     env: string = '';
-    type: 'devmove' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown' = 'unknown';
+    type: 'devmode' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown' = 'unknown';
     memoryInfo: string = '';
     cpuInfo: string = '';
     created: string = '';
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 6004cc97..34430a6b 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
@@ -43,7 +43,7 @@ export class ProjectService {
 
     public static stopDevModeContainer(project: Project) {
         useDevModeStore.setState({status: 'wip'})
-        KaravanApi.manageContainer('dev', 'devmove', project.projectId, 'stop', res => {
+        KaravanApi.manageContainer('dev', 'devmode', project.projectId, 'stop', res => {
             useDevModeStore.setState({status: 'none'})
             if (res.status === 200) {
                 useLogStore.setState({showLog: false, type: 'container'})
@@ -55,7 +55,7 @@ export class ProjectService {
 
     public static pauseDevModeContainer(project: Project) {
         useDevModeStore.setState({status: 'wip'})
-        KaravanApi.manageContainer('dev', 'devmove', project.projectId, 'pause', res => {
+        KaravanApi.manageContainer('dev', 'devmode', project.projectId, 'pause', res => {
             useDevModeStore.setState({status: 'none'})
             if (res.status === 200) {
                 useLogStore.setState({showLog: false, type: 'container'})
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx b/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
index e18284c5..dd061f75 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
@@ -1,18 +1,11 @@
 import React from 'react';
-import {Button, Flex, FlexItem, Label, Spinner, Tooltip, TooltipPosition} from '@patternfly/react-core';
+import {Badge, Button, Flex, FlexItem, Label, Spinner, Tooltip, TooltipPosition} from '@patternfly/react-core';
 import '../designer/karavan.css';
-import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon";
 import {useAppConfigStore, useDevModeStore, useLogStore, useProjectStore, useStatusesStore} from "../api/ProjectStore";
 import {shallow} from "zustand/shallow";
-import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
+import UpIcon from "@patternfly/react-icons/dist/esm/icons/running-icon";
 import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
-import RunIcon from "@patternfly/react-icons/dist/esm/icons/play-icon";
-import {ProjectService} from "../api/ProjectService";
-import ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
-import {KaravanApi} from "../api/KaravanApi";
-import {ProjectEventBus} from "../api/ProjectEventBus";
-import {EventBus} from "../designer/utils/EventBus";
-import StopIcon from "@patternfly/react-icons/dist/js/icons/stop-icon";
+import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon";
 
 interface Props {
     reloadOnly?: boolean
@@ -27,7 +20,6 @@ export function BuildToolbar (props: Props) {
     const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
 
     const containerStatus = containers.filter(c => c.containerName === project.projectId).at(0);
-    const commands = containerStatus?.commands || ['run'];
     const isRunning = containerStatus?.state === 'running';
     const inTransit = containerStatus?.inTransit;
     const isLoading = status === 'wip';
@@ -36,63 +28,20 @@ export function BuildToolbar (props: Props) {
 
     return (<Flex className="toolbar" direction={{default: "row"}} alignItems={{default: "alignItemsCenter"}}>
         <FlexItem>
-            {(inTransit || isLoading) && <Spinner size="lg" aria-label="spinner"/>}
+            <Button style={{visibility:"hidden"}} size="sm" variant={"control"} icon={<DeleteIcon/>} onClick={() => {}}>
+            </Button>
         </FlexItem>
         {containerStatus?.containerId && <FlexItem>
             <Label icon={icon} color={color}>
                 <Tooltip content={"Show log"} position={TooltipPosition.bottom}>
                     <Button className='labeled-button' variant="link" isDisabled={!isRunning}
                             onClick={e =>
-                                setShowLog(true, 'container', containerStatus.containerName)}>
+                                setShowLog( true, 'container', containerStatus.containerName)}>
                         {containerStatus.containerName}
                     </Button>
                 </Tooltip>
+                <Badge isRead>{containerStatus.type}</Badge>
             </Label>
         </FlexItem>}
-        {!isRunning && <FlexItem>
-            <Tooltip content="Run container" position={TooltipPosition.bottom}>
-                <Button size="sm"
-                        isDisabled={(!(commands.length === 0) && !commands.includes('run')) || inTransit}
-                        variant={"primary"}
-                        icon={<RunIcon/>}
-                        onClick={() => {
-                            KaravanApi.manageContainer('dev', 'project', project.projectId, 'run', res => {
-                                setShowLog(false, 'container', undefined)
-                            });
-                        }}>
-                    {"Run"}
-                </Button>
-            </Tooltip>
-        </FlexItem>}
-        {config.infrastructure !== 'kubernetes' &&
-            <FlexItem>
-                <Tooltip content="Stop container" position={TooltipPosition.bottom}>
-                    <Button size="sm"
-                            isDisabled={!commands.includes('stop') || inTransit}
-                            variant={"control"}
-                            icon={<StopIcon/>}
-                            onClick={() => {
-                                KaravanApi.manageContainer('dev', 'project', project.projectId, 'stop', res => {
-                                    setShowLog(false, 'container', undefined)
-                                });
-                            }}>
-                    </Button>
-                </Tooltip>
-            </FlexItem>
-        }
-        <FlexItem>
-            <Tooltip content="Delete container" position={TooltipPosition.bottom}>
-                <Button size="sm"
-                        isDisabled={!commands.includes('delete') || inTransit}
-                        variant={"control"}
-                        icon={<DeleteIcon/>}
-                        onClick={() => {
-                            KaravanApi.manageContainer('dev', 'project', project.projectId, 'delete', res => {
-                                setShowLog(false, 'container', undefined)
-                            });
-                        }}>
-                </Button>
-            </Tooltip>
-        </FlexItem>
-    </Flex>);
+    </Flex>)
 }
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx b/karavan-web/karavan-app/src/main/webui/src/project/ContainerPanel.tsx
similarity index 98%
copy from karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
copy to karavan-web/karavan-app/src/main/webui/src/project/ContainerPanel.tsx
index e18284c5..c32fb4a7 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/ContainerPanel.tsx
@@ -18,7 +18,7 @@ interface Props {
     reloadOnly?: boolean
 }
 
-export function BuildToolbar (props: Props) {
+export function ContainerPanel (props: Props) {
 
     const [config] = useAppConfigStore((state) => [state.config], shallow)
     const [status] = useDevModeStore((state) => [state.status], shallow)
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx b/karavan-web/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
index a659487a..2705447b 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
@@ -1,5 +1,5 @@
 import React, {useState} from 'react';
-import {Button, Flex, FlexItem, Label, Spinner, Switch, Tooltip, TooltipPosition} from '@patternfly/react-core';
+import {Badge, Button, Flex, FlexItem, Label, Spinner, Switch, Tooltip, TooltipPosition} from '@patternfly/react-core';
 import '../designer/karavan.css';
 import RocketIcon from "@patternfly/react-icons/dist/esm/icons/rocket-icon";
 import ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
@@ -7,9 +7,8 @@ import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon";
 import {useAppConfigStore, useDevModeStore, useLogStore, useProjectStore, useStatusesStore} from "../api/ProjectStore";
 import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
-import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
+import UpIcon from "@patternfly/react-icons/dist/esm/icons/running-icon";
 import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
-import StopIcon from "@patternfly/react-icons/dist/js/icons/stop-icon";
 
 interface Props {
     reloadOnly?: boolean
@@ -22,6 +21,7 @@ export function DevModeToolbar (props: Props) {
     const [project] = useProjectStore((state) => [state.project], shallow)
     const [containers] = useStatusesStore((state) => [state.containers], shallow);
     const [verbose, setVerbose] = useState(false);
+    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
 
     const containerStatus = containers.filter(c => c.containerName === project.projectId).at(0);
     const commands = containerStatus?.commands || ['run'];
@@ -30,7 +30,13 @@ export function DevModeToolbar (props: Props) {
     const isLoading = status === 'wip';
     const color = containerStatus?.state === 'running' ? "green" : "grey";
     const icon = isRunning ? <UpIcon/> : <DownIcon/>;
+    const inDevMode = containerStatus?.type === 'devmode';
+
     return (<Flex className="toolbar" direction={{default: "row"}} alignItems={{default: "alignItemsCenter"}}>
+        <FlexItem>
+            <Button style={{visibility:"hidden"}} size="sm" variant={"control"} icon={<DeleteIcon/>} onClick={() => {}}>
+            </Button>
+        </FlexItem>
         <FlexItem>
             {(inTransit || isLoading) && <Spinner size="lg" aria-label="spinner"/>}
         </FlexItem>
@@ -39,10 +45,11 @@ export function DevModeToolbar (props: Props) {
                 <Tooltip content={"Show log"} position={TooltipPosition.bottom}>
                     <Button className='labeled-button' variant="link" isDisabled={!isRunning}
                             onClick={e =>
-                                useLogStore.setState({showLog: true, type: 'container', podName: containerStatus.containerName})}>
+                                setShowLog( true, 'container', containerStatus.containerName)}>
                         {containerStatus.containerName}
                     </Button>
                 </Tooltip>
+                <Badge isRead>{containerStatus.type}</Badge>
             </Label>
         </FlexItem>}
         {!isRunning && <FlexItem>
@@ -65,7 +72,7 @@ export function DevModeToolbar (props: Props) {
                 </Button>
             </Tooltip>
         </FlexItem>}
-        {isRunning && <FlexItem>
+        {isRunning && inDevMode && <FlexItem>
             <Tooltip content="Reload" position={TooltipPosition.bottom}>
                 <Button size="sm"
                         isDisabled={inTransit}
@@ -76,19 +83,7 @@ export function DevModeToolbar (props: Props) {
                 </Button>
             </Tooltip>
         </FlexItem>}
-        {/*{config.infrastructure !== 'kubernetes' &&*/}
-        {/*    <FlexItem>*/}
-        {/*        <Tooltip content="Stop container" position={TooltipPosition.bottom}>*/}
-        {/*            <Button size="sm"*/}
-        {/*                    isDisabled={!commands.includes('stop') || inTransit}*/}
-        {/*                    variant={"control"}*/}
-        {/*                    icon={<StopIcon/>}*/}
-        {/*                    onClick={() => ProjectService.stopDevModeContainer(project)}>*/}
-        {/*            </Button>*/}
-        {/*        </Tooltip>*/}
-        {/*    </FlexItem>*/}
-        {/*}*/}
-        <FlexItem>
+        {inDevMode && <FlexItem>
             <Tooltip content="Delete container" position={TooltipPosition.bottom}>
                 <Button size="sm"
                         isDisabled={!commands.includes('delete') || inTransit}
@@ -97,6 +92,6 @@ export function DevModeToolbar (props: Props) {
                         onClick={() => ProjectService.deleteDevModeContainer(project)}>
                 </Button>
             </Tooltip>
-        </FlexItem>
+        </FlexItem>}
     </Flex>);
 }
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx
index 9c829267..31fc581d 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx
@@ -149,9 +149,9 @@ export function ImagesPanel () {
                 <Thead>
                     <Tr>
                         <Th key='status' width={10}></Th>
-                        <Th key='image' width={30}>Image</Th>
+                        <Th key='image' width={20}>Image</Th>
                         <Th key='tag' width={10}>Tag</Th>
-                        <Th key='actions'></Th>
+                        <Th key='actions' width={10}></Th>
                     </Tr>
                 </Thead>
                 <Tbody>


[camel-karavan] 01/05: Run prod container for #817

Posted by ma...@apache.org.
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 b9985be44545422ab24fd62852f045fc9234e096
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Sep 8 17:52:42 2023 -0400

    Run prod container for #817
---
 karavan-designer/src/designer/karavan.css          |   3 +-
 ...ructureResource.java => ContainerResource.java} | 190 +++------------------
 .../apache/camel/karavan/api/ImagesResource.java   |  15 ++
 .../camel/karavan/api/InfrastructureResource.java  | 154 +----------------
 .../camel/karavan/docker/DockerForKaravan.java     |  18 +-
 .../apache/camel/karavan/docker/DockerService.java |  46 +++--
 .../karavan/infinispan/model/ContainerStatus.java  |  33 ++++
 .../apache/camel/karavan/service/CamelService.java |  27 +--
 .../karavan/service/ContainerStatusService.java    |  56 ++++++
 .../camel/karavan/service/ProjectService.java      |   8 +
 .../camel/karavan/service/ScheduledService.java    | 100 -----------
 .../camel-main-docker-application.properties       |   3 +
 .../src/main/webui/src/api/KaravanApi.tsx          |  27 ++-
 .../src/main/webui/src/api/ProjectModels.ts        |   2 +-
 .../src/main/webui/src/project/BuildToolbar.tsx    |  84 ++++++++-
 .../webui/src/project/build/ProjectBuildTab.tsx    |   2 +-
 .../src/project/trace/RunnerInfoTraceModal.tsx     |   1 -
 17 files changed, 288 insertions(+), 481 deletions(-)

diff --git a/karavan-designer/src/designer/karavan.css b/karavan-designer/src/designer/karavan.css
index 11324971..bf4d9d49 100644
--- a/karavan-designer/src/designer/karavan.css
+++ b/karavan-designer/src/designer/karavan.css
@@ -38,7 +38,8 @@
     height: 36px;
 }
 
-.karavan .pf-v5-c-switch__input:focus ~ .pf-v5-c-switch__toggle {
+.karavan .pf-v5-c-switch__input:focus ~ .pf-v5-c-switch__toggle,
+.pf-v5-c-modal-box .pf-v5-c-switch__input:focus ~ .pf-v5-c-switch__toggle {
     outline: transparent;
     outline-offset: 0;
 }
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/ContainerResource.java
similarity index 52%
copy from karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
copy to karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ContainerResource.java
index f6408f84..8a237d20 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/ContainerResource.java
@@ -20,26 +20,23 @@ import io.smallrye.mutiny.Multi;
 import io.vertx.core.json.JsonObject;
 import io.vertx.mutiny.core.eventbus.EventBus;
 import io.vertx.mutiny.core.eventbus.Message;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.camel.karavan.code.CodeService;
 import org.apache.camel.karavan.code.DockerComposeConverter;
+import org.apache.camel.karavan.code.model.DockerComposeService;
 import org.apache.camel.karavan.docker.DockerForKaravan;
 import org.apache.camel.karavan.docker.DockerService;
-import org.apache.camel.karavan.code.model.DockerComposeService;
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
-import org.apache.camel.karavan.infinispan.model.DeploymentStatus;
-import org.apache.camel.karavan.infinispan.model.Project;
-import org.apache.camel.karavan.infinispan.model.ServiceStatus;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.code.CodeService;
-import org.apache.camel.karavan.service.ProjectService;
 import org.apache.camel.karavan.service.ConfigService;
+import org.apache.camel.karavan.service.ProjectService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
-import jakarta.inject.Inject;
-import jakarta.ws.rs.*;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
@@ -47,8 +44,8 @@ import java.util.stream.Collectors;
 
 import static org.apache.camel.karavan.service.ContainerStatusService.CONTAINER_STATUS;
 
-@Path("/api/infrastructure")
-public class InfrastructureResource {
+@Path("/api/container")
+public class ContainerResource {
 
     @Inject
     EventBus eventBus;
@@ -74,109 +71,10 @@ public class InfrastructureResource {
     @ConfigProperty(name = "karavan.environment")
     String environment;
 
-    private static final Logger LOGGER = Logger.getLogger(InfrastructureResource.class.getName());
-
-    @POST
-    @Produces(MediaType.APPLICATION_JSON)
-    @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/pipeline/{env}")
-    public String createPipeline(@PathParam("env") String env, Project project) throws Exception {
-        Project p = infinispanService.getProject(project.getProjectId());
-        return kubernetesService.createPipelineRun(project);
-    }
+    private static final Logger LOGGER = Logger.getLogger(ContainerResource.class.getName());
 
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("/pipeline/{env}/{name}")
-    public Response getPipeline(@PathParam("env") String env,
-                                @PathParam("name") String name) throws Exception {
-        return Response.ok(kubernetesService.getPipelineRun(name, kubernetesService.getNamespace())).build();
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/pipeline/log/{env}/{name}")
-    public Response getPipelineLog(@PathParam("env") String env,
-                                   @PathParam("name") String name) throws Exception {
-        return Response.ok(kubernetesService.getPipelineRunLog(name, kubernetesService.getNamespace())).build();
-    }
-
-    @DELETE
-    @Produces(MediaType.APPLICATION_JSON)
-    @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/pipelinerun/{env}/{name}")
-    public Response stopPipelineRun(@PathParam("env") String env, @PathParam("name") String name) throws Exception {
-        kubernetesService.stopPipelineRun(name, kubernetesService.getNamespace());
-        return Response.ok().build();
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/container/log/{env}/{name}")
-    public Response getContainerLog(@PathParam("env") String env,
-                                    @PathParam("name") String name) throws Exception {
-        return Response.ok(kubernetesService.getContainerLog(name, kubernetesService.getNamespace())).build();
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/deployment")
-    public List<DeploymentStatus> getAllDeploymentStatuses() throws Exception {
-        if (infinispanService.isReady()) {
-            return infinispanService.getDeploymentStatuses().stream()
-                    .sorted(Comparator.comparing(DeploymentStatus::getProjectId))
-                    .collect(Collectors.toList());
-        } else {
-            return List.of();
-        }
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/deployment/{env}")
-    public List<DeploymentStatus> getDeploymentStatusesByEnv(@PathParam("env") String env) throws Exception {
-        if (infinispanService.isReady()) {
-        return infinispanService.getDeploymentStatuses(env).stream()
-                .sorted(Comparator.comparing(DeploymentStatus::getProjectId))
-                .collect(Collectors.toList());
-        } else {
-            return List.of();
-        }
-    }
-
-    @POST
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/deployment/rollout/{env}/{name}")
-    public Response rollout(@PathParam("env") String env, @PathParam("name") String name) throws Exception {
-        kubernetesService.rolloutDeployment(name, kubernetesService.getNamespace());
-        return Response.ok().build();
-    }
-
-    @DELETE
-    @Produces(MediaType.APPLICATION_JSON)
-    @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/deployment/{env}/{name}")
-    public Response deleteDeployment(@PathParam("env") String env, @PathParam("name") String name) throws Exception {
-        kubernetesService.deleteDeployment(name, kubernetesService.getNamespace());
-        return Response.ok().build();
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/service")
-    public List<ServiceStatus> getAllServiceStatuses() throws Exception {
-        if (infinispanService.isReady()) {
-            return infinispanService.getServiceStatuses().stream()
-                    .sorted(Comparator.comparing(ServiceStatus::getProjectId))
-                    .collect(Collectors.toList());
-        } else {
-            return List.of();
-        }
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/container")
     public List<ContainerStatus> getAllContainerStatuses() throws Exception {
         if (infinispanService.isReady()) {
             return infinispanService.getContainerStatuses().stream()
@@ -190,7 +88,7 @@ public class InfrastructureResource {
     @POST
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/container/{env}/{type}/{name}")
+    @Path("/{env}/{type}/{name}")
     public Response manageContainer(@PathParam("env") String env, @PathParam("type") String type, @PathParam("name") String name, JsonObject command) throws Exception {
         if (infinispanService.isReady()) {
             // set container statuses
@@ -202,7 +100,13 @@ public class InfrastructureResource {
                         String code = projectService.getDevServiceCode();
                         DockerComposeService dockerComposeService = DockerComposeConverter.fromCode(code, name);
                         if (dockerComposeService != null) {
-                            dockerForKaravan.createDevserviceContainer(dockerComposeService);
+                            dockerService.createContainerFromCompose(dockerComposeService, ContainerStatus.ContainerType.devmode);
+                            dockerService.runContainer(dockerComposeService.getContainer_name());
+                        }
+                    } else if (Objects.equals(type, ContainerStatus.ContainerType.project.name())) {
+                        DockerComposeService dockerComposeService = projectService.getProjectDockerComposeService(name);
+                        if (dockerComposeService != null) {
+                            dockerService.createContainerFromCompose(dockerComposeService, ContainerStatus.ContainerType.project);
                             dockerService.runContainer(dockerComposeService.getContainer_name());
                         }
                     } else if (Objects.equals(type, ContainerStatus.ContainerType.devmode.name())) {
@@ -217,6 +121,9 @@ public class InfrastructureResource {
                 } else if (command.getString("command").equalsIgnoreCase("pause")) {
                     dockerService.pauseContainer(name);
                     return Response.ok().build();
+                } else if (command.getString("command").equalsIgnoreCase("delete")) {
+                    dockerService.deleteContainer(name);
+                    return Response.ok().build();
                 }
             }
         }
@@ -234,7 +141,7 @@ public class InfrastructureResource {
 
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("/container/{env}")
+    @Path("/{env}")
     public List<ContainerStatus> getContainerStatusesByEnv(@PathParam("env") String env) throws Exception {
         return infinispanService.getContainerStatuses(env).stream()
                 .sorted(Comparator.comparing(ContainerStatus::getProjectId))
@@ -243,7 +150,7 @@ public class InfrastructureResource {
 
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("/container/{projectId}/{env}")
+    @Path("/{projectId}/{env}")
     public List<ContainerStatus> getContainerStatusesByProjectAndEnv(@PathParam("projectId") String projectId, @PathParam("env") String env) throws Exception {
         return infinispanService.getContainerStatuses(projectId, env).stream()
                 .filter(podStatus -> Objects.equals(podStatus.getType(), ContainerStatus.ContainerType.project))
@@ -254,7 +161,7 @@ public class InfrastructureResource {
     @DELETE
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/container/{env}/{type}/{name}")
+    @Path("/{env}/{type}/{name}")
     public Response deleteContainer(@PathParam("env") String env, @PathParam("type") String type, @PathParam("name") String name) {
         if (infinispanService.isReady()) {
             // set container statuses
@@ -274,56 +181,9 @@ public class InfrastructureResource {
         return Response.notModified().build();
     }
 
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/imagetag/{env}/{projectId}")
-    public Response getProjectImageTags(@PathParam("env") String env, @PathParam("projectId") String projectId) throws Exception {
-        return Response.ok(kubernetesService.getProjectImageTags(projectId, kubernetesService.getNamespace())).build();
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/configmaps")
-    public Response getConfigMaps() throws Exception {
-        if (ConfigService.inKubernetes()) {
-            return Response.ok(kubernetesService.getConfigMaps(kubernetesService.getNamespace())).build();
-        } else {
-            return Response.ok(List.of()).build();
-        }
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/secrets")
-    public Response getSecrets() throws Exception {
-        if (ConfigService.inKubernetes()) {
-            return Response.ok(kubernetesService.getSecrets(kubernetesService.getNamespace())).build();
-        } else {
-            return Response.ok(List.of()).build();
-        }
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/services")
-    public Response getServices() throws Exception {
-        if (infinispanService.isReady()) {
-            if (ConfigService.inKubernetes()) {
-                return Response.ok(kubernetesService.getServices(kubernetesService.getNamespace())).build();
-            } else {
-                List<String> list = infinispanService.getContainerStatuses(environment).stream()
-                        .map(ci -> ci.getPorts().stream().map(i -> ci.getContainerName() + ":" + i).collect(Collectors.toList()))
-                        .flatMap(List::stream).collect(Collectors.toList());
-                return Response.ok(list).build();
-            }
-        } else {
-            return Response.ok(List.of()).build();
-        }
-    }
-
     // TODO: implement log watch
     @GET
-    @Path("/container/log/watch/{env}/{name}")
+    @Path("/log/watch/{env}/{name}")
     @Produces(MediaType.SERVER_SENT_EVENTS)
     public Multi<String> getContainerLogWatch(@PathParam("env") String env, @PathParam("name") String name) {
         LOGGER.info("Start sourcing");
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
index 9d4e0371..fe1a9f7a 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
@@ -23,9 +23,12 @@ import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
 import org.apache.camel.karavan.docker.DockerService;
 import org.apache.camel.karavan.infinispan.model.Project;
+import org.apache.camel.karavan.service.ConfigService;
 import org.apache.camel.karavan.service.ProjectService;
 import org.apache.camel.karavan.service.RegistryService;
 
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.util.Comparator;
 import java.util.List;
 
@@ -65,4 +68,16 @@ public class ImagesResource {
             return Response.serverError().entity(e.getMessage()).build();
         }
     }
+
+    @DELETE
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/{imageName}")
+    public Response deleteImage(@HeaderParam("username") String username, @PathParam("imageName") String imageName) {
+        if (ConfigService.inKubernetes()) {
+            return Response.ok().build();
+        } else {
+            dockerService.deleteImage(imageName);
+            return Response.ok().build();
+        }
+    }
 }
\ No newline at end of file
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 f6408f84..7c2a6b6a 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
@@ -16,61 +16,32 @@
  */
 package org.apache.camel.karavan.api;
 
-import io.smallrye.mutiny.Multi;
-import io.vertx.core.json.JsonObject;
-import io.vertx.mutiny.core.eventbus.EventBus;
-import io.vertx.mutiny.core.eventbus.Message;
-import org.apache.camel.karavan.code.DockerComposeConverter;
-import org.apache.camel.karavan.docker.DockerForKaravan;
-import org.apache.camel.karavan.docker.DockerService;
-import org.apache.camel.karavan.code.model.DockerComposeService;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
 import org.apache.camel.karavan.infinispan.InfinispanService;
-import org.apache.camel.karavan.infinispan.model.ContainerStatus;
 import org.apache.camel.karavan.infinispan.model.DeploymentStatus;
 import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.infinispan.model.ServiceStatus;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
-import org.apache.camel.karavan.code.CodeService;
-import org.apache.camel.karavan.service.ProjectService;
 import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
-import jakarta.inject.Inject;
-import jakarta.ws.rs.*;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Objects;
 import java.util.stream.Collectors;
 
-import static org.apache.camel.karavan.service.ContainerStatusService.CONTAINER_STATUS;
-
 @Path("/api/infrastructure")
 public class InfrastructureResource {
 
-    @Inject
-    EventBus eventBus;
-
     @Inject
     InfinispanService infinispanService;
 
     @Inject
     KubernetesService kubernetesService;
 
-    @Inject
-    DockerForKaravan dockerForKaravan;
-
-    @Inject
-    DockerService dockerService;
-
-    @Inject
-    ProjectService projectService;
-
-    @Inject
-    CodeService codeService;
-
     @ConfigProperty(name = "karavan.environment")
     String environment;
 
@@ -110,14 +81,6 @@ public class InfrastructureResource {
         return Response.ok().build();
     }
 
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/container/log/{env}/{name}")
-    public Response getContainerLog(@PathParam("env") String env,
-                                    @PathParam("name") String name) throws Exception {
-        return Response.ok(kubernetesService.getContainerLog(name, kubernetesService.getNamespace())).build();
-    }
-
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/deployment")
@@ -174,106 +137,6 @@ public class InfrastructureResource {
         }
     }
 
-    @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();
-        }
-    }
-
-    @POST
-    @Produces(MediaType.APPLICATION_JSON)
-    @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/container/{env}/{type}/{name}")
-    public Response manageContainer(@PathParam("env") String env, @PathParam("type") String type, @PathParam("name") String name, JsonObject command) throws Exception {
-        if (infinispanService.isReady()) {
-            // set container statuses
-            setContainerStatusTransit(name, type);
-            // exec docker commands
-            if (command.containsKey("command")) {
-                if (command.getString("command").equalsIgnoreCase("run")) {
-                    if (Objects.equals(type, ContainerStatus.ContainerType.devservice.name())) {
-                        String code = projectService.getDevServiceCode();
-                        DockerComposeService dockerComposeService = DockerComposeConverter.fromCode(code, name);
-                        if (dockerComposeService != null) {
-                            dockerForKaravan.createDevserviceContainer(dockerComposeService);
-                            dockerService.runContainer(dockerComposeService.getContainer_name());
-                        }
-                    } else if (Objects.equals(type, ContainerStatus.ContainerType.devmode.name())) {
-//                        TODO: merge with DevMode service
-//                        dockerForKaravan.createDevmodeContainer(name, "");
-//                        dockerService.runContainer(name);
-                    }
-                    return Response.ok().build();
-                } else if (command.getString("command").equalsIgnoreCase("stop")) {
-                    dockerService.stopContainer(name);
-                    return Response.ok().build();
-                } else if (command.getString("command").equalsIgnoreCase("pause")) {
-                    dockerService.pauseContainer(name);
-                    return Response.ok().build();
-                }
-            }
-        }
-        return Response.notModified().build();
-    }
-
-    private void setContainerStatusTransit(String name, String type){
-        ContainerStatus status = infinispanService.getContainerStatus(name, environment, name);
-        if (status == null) {
-            status = ContainerStatus.createByType(name, environment, ContainerStatus.ContainerType.valueOf(type));
-        }
-        status.setInTransit(true);
-        eventBus.send(CONTAINER_STATUS, JsonObject.mapFrom(status));
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/container/{env}")
-    public List<ContainerStatus> getContainerStatusesByEnv(@PathParam("env") String env) throws Exception {
-        return infinispanService.getContainerStatuses(env).stream()
-                .sorted(Comparator.comparing(ContainerStatus::getProjectId))
-                .collect(Collectors.toList());
-    }
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @Path("/container/{projectId}/{env}")
-    public List<ContainerStatus> getContainerStatusesByProjectAndEnv(@PathParam("projectId") String projectId, @PathParam("env") String env) throws Exception {
-        return infinispanService.getContainerStatuses(projectId, env).stream()
-                .filter(podStatus -> Objects.equals(podStatus.getType(), ContainerStatus.ContainerType.project))
-                .sorted(Comparator.comparing(ContainerStatus::getContainerName))
-                .collect(Collectors.toList());
-    }
-
-    @DELETE
-    @Produces(MediaType.APPLICATION_JSON)
-    @Consumes(MediaType.APPLICATION_JSON)
-    @Path("/container/{env}/{type}/{name}")
-    public Response deleteContainer(@PathParam("env") String env, @PathParam("type") String type, @PathParam("name") String name) {
-        if (infinispanService.isReady()) {
-            // set container statuses
-            setContainerStatusTransit(name, type);
-            try {
-                if (ConfigService.inKubernetes()) {
-                    kubernetesService.deletePod(name, kubernetesService.getNamespace());
-                } else {
-                    dockerService.deleteContainer(name);
-                }
-                return Response.accepted().build();
-            } catch (Exception e) {
-                LOGGER.error(e.getMessage());
-                return Response.notModified().build();
-            }
-        }
-        return Response.notModified().build();
-    }
-
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/imagetag/{env}/{projectId}")
@@ -320,13 +183,4 @@ public class InfrastructureResource {
             return Response.ok(List.of()).build();
         }
     }
-
-    // TODO: implement log watch
-    @GET
-    @Path("/container/log/watch/{env}/{name}")
-    @Produces(MediaType.SERVER_SENT_EVENTS)
-    public Multi<String> getContainerLogWatch(@PathParam("env") String env, @PathParam("name") String name) {
-        LOGGER.info("Start sourcing");
-        return eventBus.<String>consumer(name + "-" + kubernetesService.getNamespace()).toMulti().map(Message::body);
-    }
 }
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
index cfa74f84..41861af7 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerForKaravan.java
@@ -69,27 +69,23 @@ public class DockerForKaravan {
     }
 
     public void runBuildProject(String projectId, String script, Map<String, String> files, List<String> env,  Map<String, String> volumes, String tag) throws Exception {
-        dockerService.deleteContainer(projectId + BUILDER_SUFFIX);
-        Container c = createBuildContainer(projectId, env, volumes, tag);
+        String containerName = projectId + BUILDER_SUFFIX;
+        dockerService.deleteContainer(containerName);
+        Container c = createBuildContainer(containerName, projectId, env, volumes, tag);
         dockerService.copyFiles(c.getId(), "/code", files);
         dockerService.copyExecFile(c.getId(), "/karavan", "build.sh", script);
-        dockerService.runContainer(projectId);
+        dockerService.runContainer(c);
     }
 
-    protected Container createBuildContainer(String projectId, List<String> env, Map<String, String> volumes, String tag) throws InterruptedException {
-        LOGGER.infof("Starting Build Container for %s ", projectId);
+    protected Container createBuildContainer(String containerName, String projectId, List<String> env, Map<String, String> volumes, String tag) throws InterruptedException {
+        LOGGER.infof("Starting Build Container ", containerName);
 
-        return dockerService.createContainer(projectId + BUILDER_SUFFIX, devmodeImage,
+        return dockerService.createContainer(containerName, devmodeImage,
                 env, Map.of(), new HealthCheck(),
                 Map.of(LABEL_TYPE, ContainerStatus.ContainerType.build.name(), LABEL_PROJECT_ID, projectId, LABEL_TAG, tag),
                 volumes, null,"/karavan/build.sh");
     }
 
-    public void createDevserviceContainer(DockerComposeService dockerComposeService) throws InterruptedException {
-        LOGGER.infof("DevService starting for ", dockerComposeService.getContainer_name());
-        dockerService.createContainerFromCompose(dockerComposeService, ContainerStatus.ContainerType.devservice);
-    }
-
     public void syncImage(String projectId, String tag) throws InterruptedException {
         String image = registryService.getRegistryWithGroup() + "/" + projectId + ":" + tag;
         dockerService.pullImage(image);
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
index 0f680ad8..9d5ac75f 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
@@ -103,6 +103,7 @@ public class DockerService extends DockerServiceUtils {
     public void stopListeners() throws IOException {
         dockerEventListener.close();
     }
+
     public void createNetwork() {
         if (!getDockerClient().listNetworksCmd().exec().stream()
                 .filter(n -> n.getName().equals(NETWORK_NAME))
@@ -124,7 +125,7 @@ public class DockerService extends DockerServiceUtils {
     }
 
     public Container getContainerByName(String name) {
-        List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec();
+        List<Container> containers = findContainer(name);
         return containers.size() > 0 ? containers.get(0) : null;
     }
 
@@ -142,7 +143,7 @@ public class DockerService extends DockerServiceUtils {
     }
 
     public Container createContainerFromCompose(DockerComposeService compose, ContainerStatus.ContainerType type) throws InterruptedException {
-        List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(compose.getContainer_name())).exec();
+        List<Container> containers = findContainer(compose.getContainer_name());
         if (containers.isEmpty()) {
             LOGGER.infof("Compose Service starting for %s", compose.getContainer_name());
 
@@ -160,10 +161,15 @@ public class DockerService extends DockerServiceUtils {
         }
     }
 
+    public List<Container> findContainer(String containerName) {
+        return getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(containerName)).exec()
+                .stream().filter(c -> Objects.equals(c.getNames()[0].replaceFirst("/", ""), containerName)).toList();
+    }
+
     public Container createContainer(String name, String image, List<String> env, Map<Integer, Integer> ports,
                                      HealthCheck healthCheck, Map<String, String> labels,
                                      Map<String, String> volumes, String network, String... command) throws InterruptedException {
-        List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec();
+        List<Container> containers = findContainer(name);
         if (containers.size() == 0) {
             pullImage(image);
 
@@ -186,7 +192,7 @@ public class DockerService extends DockerServiceUtils {
             }
             createContainerCmd.withHostConfig(new HostConfig()
                     .withPortBindings(portBindings)
-                            .withMounts(mounts)
+                    .withMounts(mounts)
                     .withNetworkMode(network != null ? network : NETWORK_NAME));
 
             CreateContainerResponse response = createContainerCmd.exec();
@@ -200,14 +206,17 @@ public class DockerService extends DockerServiceUtils {
     }
 
     public void runContainer(String name) {
-        List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec();
+        List<Container> containers = findContainer(name);
         if (containers.size() == 1) {
-            Container container = containers.get(0);
-            if (container.getState().equals("paused")) {
-                getDockerClient().unpauseContainerCmd(container.getId()).exec();
-            } else if (!container.getState().equals("running")) {
-                getDockerClient().startContainerCmd(container.getId()).exec();
-            }
+            runContainer(containers.get(0));
+        }
+    }
+
+    public void runContainer(Container container) {
+        if (container.getState().equals("paused")) {
+            getDockerClient().unpauseContainerCmd(container.getId()).exec();
+        } else if (!container.getState().equals("running")) {
+            getDockerClient().startContainerCmd(container.getId()).exec();
         }
     }
 
@@ -247,10 +256,10 @@ public class DockerService extends DockerServiceUtils {
     }
 
     public void copyFiles(String containerId, String containerPath, Map<String, String> files) {
-            String temp = vertx.fileSystem().createTempDirectoryBlocking(containerId);
-            files.forEach((fileName, code) -> addFile(temp, fileName, code));
-            dockerClient.copyArchiveToContainerCmd(containerId).withRemotePath(containerPath)
-                    .withDirChildrenOnly(true).withHostResource(temp).exec();
+        String temp = vertx.fileSystem().createTempDirectoryBlocking(containerId);
+        files.forEach((fileName, code) -> addFile(temp, fileName, code));
+        dockerClient.copyArchiveToContainerCmd(containerId).withRemotePath(containerPath)
+                .withDirChildrenOnly(true).withHostResource(temp).exec();
     }
 
     public void copyExecFile(String containerId, String containerPath, String filename, String script) {
@@ -259,7 +268,7 @@ public class DockerService extends DockerServiceUtils {
         vertx.fileSystem().writeFileBlocking(path, Buffer.buffer(script));
 
         try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-                TarArchiveOutputStream tarArchive = new TarArchiveOutputStream(byteArrayOutputStream)) {
+             TarArchiveOutputStream tarArchive = new TarArchiveOutputStream(byteArrayOutputStream)) {
             tarArchive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
             tarArchive.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
 
@@ -328,7 +337,7 @@ public class DockerService extends DockerServiceUtils {
     }
 
     public void deleteContainer(String name) {
-        List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec();
+        List<Container> containers = findContainer(name);
         if (containers.size() == 1) {
             Container container = containers.get(0);
             getDockerClient().removeContainerCmd(container.getId()).withForce(true).exec();
@@ -382,4 +391,7 @@ public class DockerService extends DockerServiceUtils {
         return getDockerClient().listImagesCmd().withShowAll(true).exec().stream()
                 .map(image -> image.getRepoTags()[0]).toList();
     }
+
+    public void deleteImage(String imageName) {
+    }
 }
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
index ab7c04b8..e734abc4 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
@@ -4,6 +4,7 @@ import org.infinispan.protostream.annotations.ProtoEnumValue;
 import org.infinispan.protostream.annotations.ProtoFactory;
 import org.infinispan.protostream.annotations.ProtoField;
 
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -65,8 +66,29 @@ public class ContainerStatus {
     Boolean codeLoaded;
     @ProtoField(number = 15)
     Boolean inTransit = false;
+    @ProtoField(number = 16)
+    String initDate;
 
     @ProtoFactory
+    public ContainerStatus(String projectId, String containerName, String containerId, String image, List<Integer> ports, String env, ContainerType type, String memoryInfo, String cpuInfo, String created, String finished, List<Command> commands, String state, Boolean codeLoaded, Boolean inTransit, String initDate) {
+        this.projectId = projectId;
+        this.containerName = containerName;
+        this.containerId = containerId;
+        this.image = image;
+        this.ports = ports;
+        this.env = env;
+        this.type = type;
+        this.memoryInfo = memoryInfo;
+        this.cpuInfo = cpuInfo;
+        this.created = created;
+        this.finished = finished;
+        this.commands = commands;
+        this.state = state;
+        this.codeLoaded = codeLoaded;
+        this.inTransit = inTransit;
+        this.initDate = initDate;
+    }
+
     public ContainerStatus(String projectId, String containerName, String containerId, String image, List<Integer> ports, String env, ContainerType type, String memoryInfo, String cpuInfo, String created, String finished, List<Command> commands, String state, Boolean codeLoaded, Boolean inTransit) {
         this.projectId = projectId;
         this.containerName = containerName;
@@ -83,6 +105,7 @@ public class ContainerStatus {
         this.state = state;
         this.codeLoaded = codeLoaded;
         this.inTransit = inTransit;
+        this.initDate = Instant.now().toString();
     }
 
     public ContainerStatus(String containerName, List<Command> commands, String projectId, String env, ContainerType type, String memoryInfo, String cpuInfo, String created) {
@@ -94,6 +117,7 @@ public class ContainerStatus {
         this.memoryInfo = memoryInfo;
         this.cpuInfo = cpuInfo;
         this.created = created;
+        this.initDate = Instant.now().toString();
     }
 
     public ContainerStatus(String containerName, List<Command> commands, String projectId, String env, ContainerType type, String created) {
@@ -103,6 +127,7 @@ public class ContainerStatus {
         this.env = env;
         this.created = created;
         this.type = type;
+        this.initDate = Instant.now().toString();
     }
 
     public static ContainerStatus createDevMode(String projectId, String env) {
@@ -242,6 +267,14 @@ public class ContainerStatus {
         this.finished = finished;
     }
 
+    public String getInitDate() {
+        return initDate;
+    }
+
+    public void setInitDate(String initDate) {
+        this.initDate = initDate;
+    }
+
     @Override
     public String toString() {
         return "ContainerStatus{" +
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelService.java
index 7f054871..8bc62635 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.karavan.service;
 
+import io.quarkus.scheduler.Scheduled;
 import io.quarkus.vertx.ConsumeEvent;
 import io.vertx.core.json.JsonObject;
 import io.vertx.mutiny.core.Vertx;
@@ -70,6 +71,19 @@ public class CamelService {
         return webClient;
     }
 
+    @Scheduled(every = "{karavan.camel.status.interval}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
+    public void collectCamelStatuses() {
+        if (infinispanService.isReady()) {
+            infinispanService.getContainerStatuses(environment).stream()
+                    .filter(cs ->
+                            cs.getType() == ContainerStatus.ContainerType.project
+                                    || cs.getType() == ContainerStatus.ContainerType.devmode
+                    ).forEach(pod -> {
+                        CamelStatusRequest csr = new CamelStatusRequest(pod.getProjectId(), pod.getContainerName());
+                        eventBus.publish(CMD_COLLECT_CAMEL_STATUS, JsonObject.mapFrom(csr));
+                    });
+        }
+    }
 
     private boolean camelIsStarted(CamelStatus camelStatus) {
         try {
@@ -129,19 +143,6 @@ public class CamelService {
         }
     }
 
-    public void collectCamelStatuses() {
-        if (infinispanService.isReady()) {
-            infinispanService.getContainerStatuses(environment).stream()
-                    .filter(cs ->
-                            cs.getType() == ContainerStatus.ContainerType.project
-                            || cs.getType() == ContainerStatus.ContainerType.devmode
-                    ).forEach(pod -> {
-                CamelStatusRequest csr = new CamelStatusRequest(pod.getProjectId(), pod.getContainerName());
-                eventBus.publish(CMD_COLLECT_CAMEL_STATUS, JsonObject.mapFrom(csr));
-            });
-        }
-    }
-
     @ConsumeEvent(value = CMD_COLLECT_CAMEL_STATUS, blocking = true, ordered = true)
     public void collectCamelStatuses(JsonObject data) {
         CamelStatusRequest dms = data.mapTo(CamelStatusRequest.class);
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ContainerStatusService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ContainerStatusService.java
index 5fc126eb..c3b310bc 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ContainerStatusService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ContainerStatusService.java
@@ -1,14 +1,20 @@
 package org.apache.camel.karavan.service;
 
+import io.quarkus.scheduler.Scheduled;
 import io.quarkus.vertx.ConsumeEvent;
+import io.vertx.core.eventbus.EventBus;
 import io.vertx.core.json.JsonObject;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
+import org.apache.camel.karavan.docker.DockerService;
 import org.apache.camel.karavan.infinispan.InfinispanService;
 import org.apache.camel.karavan.infinispan.model.ContainerStatus;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
 import java.util.Objects;
 
 @ApplicationScoped
@@ -16,10 +22,60 @@ public class ContainerStatusService {
 
     public static final String CONTAINER_STATUS = "CONTAINER_STATUS";
     private static final Logger LOGGER = Logger.getLogger(ContainerStatusService.class.getName());
+    @ConfigProperty(name = "karavan.environment")
+    String environment;
 
     @Inject
     InfinispanService infinispanService;
 
+    @Inject
+    DockerService dockerService;
+
+    @Inject
+    EventBus eventBus;
+
+    @Scheduled(every = "{karavan.container.statistics.interval}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
+    void collectContainersStatistics() {
+        if (infinispanService.isReady()) {
+            List<ContainerStatus> statusesInDocker = dockerService.collectContainersStatistics();
+            statusesInDocker.forEach(containerStatus -> {
+                eventBus.send(ContainerStatusService.CONTAINER_STATUS, JsonObject.mapFrom(containerStatus));
+            });
+        }
+    }
+
+    @Scheduled(every = "{karavan.container.status.interval}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
+    void collectContainersStatuses() {
+        if (infinispanService.isReady()) {
+            List<ContainerStatus> statusesInDocker = dockerService.collectContainersStatuses();
+            statusesInDocker.forEach(containerStatus -> {
+                eventBus.send(ContainerStatusService.CONTAINER_STATUS, JsonObject.mapFrom(containerStatus));
+            });
+            cleanContainersStatuses(statusesInDocker);
+        }
+    }
+
+    void cleanContainersStatuses(List<ContainerStatus> statusesInDocker) {
+        if (infinispanService.isReady()) {
+            List<String> namesInDocker = statusesInDocker.stream().map(ContainerStatus::getContainerName).toList();
+            List<ContainerStatus> statusesInInfinispan = infinispanService.getContainerStatuses(environment);
+            // clean deleted
+            statusesInInfinispan.stream()
+                    .filter(cs -> !checkTransit(cs))
+                    .filter(cs -> !namesInDocker.contains(cs.getContainerName()))
+                    .forEach(containerStatus -> {
+                        infinispanService.deleteContainerStatus(containerStatus);
+                        infinispanService.deleteCamelStatuses(containerStatus.getProjectId(), containerStatus.getEnv());
+                    });
+        }
+    }
+
+    private boolean checkTransit(ContainerStatus cs) {
+        if (cs.getContainerId() == null && cs.getInTransit()) {
+            return Instant.parse(cs.getInitDate()).until(Instant.now(), ChronoUnit.SECONDS) < 10;
+        }
+        return false;
+    }
 
     @ConsumeEvent(value = CONTAINER_STATUS, blocking = true, ordered = true)
     public void saveContainerStatus(JsonObject data) {
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
index 5ce1e34f..ebd49e08 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
@@ -387,4 +387,12 @@ public class ProjectService implements HealthCheck {
             }
         }
     }
+
+    public DockerComposeService getProjectDockerComposeService(String projectId) {
+        ProjectFile file = infinispanService.getProjectFile(projectId, PROJECT_COMPOSE_FILENAME);
+        if (file != null) {
+            return DockerComposeConverter.fromCode(file.getCode(), projectId);
+        }
+        return null;
+    }
 }
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java
deleted file mode 100644
index 1a90dc6c..00000000
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.camel.karavan.service;
-
-import io.quarkus.scheduler.Scheduled;
-import io.vertx.core.eventbus.EventBus;
-import io.vertx.core.json.JsonObject;
-import org.apache.camel.karavan.docker.DockerForInfinispan;
-import org.apache.camel.karavan.docker.DockerService;
-import org.apache.camel.karavan.infinispan.InfinispanService;
-import org.apache.camel.karavan.infinispan.model.ContainerStatus;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.jboss.logging.Logger;
-
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import java.util.List;
-
-@ApplicationScoped
-public class ScheduledService {
-
-    private static final Logger LOGGER = Logger.getLogger(ScheduledService.class.getName());
-
-    @ConfigProperty(name = "karavan.environment")
-    String environment;
-
-    @Inject
-    DockerService dockerService;
-
-    @Inject
-    DockerForInfinispan dockerForInfinispan;
-
-    @Inject
-    ProjectService projectService;
-
-    @Inject
-    CamelService camelService;
-
-    @Inject
-    InfinispanService infinispanService;
-
-    @Inject
-    EventBus eventBus;
-
-    @Scheduled(every = "{karavan.container.statistics.interval}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
-    void collectContainersStatistics() {
-        if (infinispanService.isReady()) {
-            List<ContainerStatus> statusesInDocker = dockerService.collectContainersStatistics();
-            statusesInDocker.forEach(containerStatus -> {
-                eventBus.send(ContainerStatusService.CONTAINER_STATUS, JsonObject.mapFrom(containerStatus));
-            });
-        }
-    }
-
-    @Scheduled(every = "{karavan.container.status.interval}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
-    void collectContainersStatuses() {
-        if (infinispanService.isReady()) {
-            List<ContainerStatus> statusesInDocker = dockerService.collectContainersStatuses();
-            statusesInDocker.forEach(containerStatus -> {
-                eventBus.send(ContainerStatusService.CONTAINER_STATUS, JsonObject.mapFrom(containerStatus));
-            });
-            cleanContainersStatuses(statusesInDocker);
-        }
-    }
-
-    void cleanContainersStatuses(List<ContainerStatus> statusesInDocker) {
-        if (infinispanService.isReady()) {
-            List<String> namesInDocker = statusesInDocker.stream().map(ContainerStatus::getContainerName).toList();
-            List<ContainerStatus> statusesInInfinispan = infinispanService.getContainerStatuses(environment);
-            // clean deleted
-            statusesInInfinispan.stream()
-                    .filter(cs -> !(cs.getContainerId() == null && cs.getInTransit()))
-                    .filter(cs -> !namesInDocker.contains(cs.getContainerName()))
-                    .forEach(containerStatus -> {
-                        infinispanService.deleteContainerStatus(containerStatus);
-                        infinispanService.deleteCamelStatuses(containerStatus.getProjectId(), containerStatus.getEnv());
-                    });
-        }
-    }
-
-
-    @Scheduled(every = "{karavan.camel.status.interval}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
-    void collectCamelStatuses() {
-        camelService.collectCamelStatuses();
-    }
-}
diff --git a/karavan-web/karavan-app/src/main/resources/snippets/camel-main-docker-application.properties b/karavan-web/karavan-app/src/main/resources/snippets/camel-main-docker-application.properties
index 5048638c..2ce5adb2 100644
--- a/karavan-web/karavan-app/src/main/resources/snippets/camel-main-docker-application.properties
+++ b/karavan-web/karavan-app/src/main/resources/snippets/camel-main-docker-application.properties
@@ -5,6 +5,9 @@ camel.jbang.gav=org.camel.karavan.demo:{projectId}:1
 camel.jbang.runtime=camel-main
 camel.jbang.version=4.0.0-RC2
 camel.jbang.dependencies=camel-console,camel-platform-http-main
+camel.context.dev-console=true
+camel.main.backlogTracing=true
+camel.main.beanIntrospectionExtendedStatistics=true
 camel.health.enabled=true
 camel.health.exposure-level=full
 camel.server.enabled=true
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 d6bcfcd2..fd66d5cd 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
@@ -412,7 +412,7 @@ export class KaravanApi {
     }
 
     static async getContainerLog(environment: string, name: string, after: (res: AxiosResponse<string>) => void) {
-        instance.get('/api/infrastructure/container/log/' + environment + "/" + name)
+        instance.get('/api/container/log/' + environment + "/" + name)
             .then(res => {
                 if (res.status === 200) {
                     after(res.data);
@@ -434,7 +434,7 @@ export class KaravanApi {
     }
 
     static async getAllContainerStatuses(after: (statuses: ContainerStatus[]) => void) {
-        instance.get('/api/infrastructure/container')
+        instance.get('/api/container')
             .then(res => {
                 if (res.status === 200) {
                     after(res.data);
@@ -484,19 +484,12 @@ export class KaravanApi {
         });
     }
 
-    static async getProjectPodStatuses(project: string, env: string, after: (statuses: ContainerStatus[]) => void) {
-        instance.get('/api/infrastructure/container/' + project + "/" + env)
-            .then(res => {
-                if (res.status === 200) {
-                    after(res.data);
-                }
-            }).catch(err => {
-            console.log(err);
-        });
-    }
-
-    static async manageContainer(environment: string, type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown',  name: string, command: 'run' | 'pause' | 'stop', after: (res: AxiosResponse<any>) => void) {
-        instance.post('/api/infrastructure/container/' + environment + '/' + type + "/" + name, {command: command})
+    static async manageContainer(environment: string,
+                                 type: 'devmove' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown',
+                                 name: string,
+                                 command: 'run' | 'pause' | 'stop' | 'delete',
+                                 after: (res: AxiosResponse<any>) => void) {
+        instance.post('/api/container/' + environment + '/' + type + "/" + name, {command: command})
             .then(res => {
                 after(res);
             }).catch(err => {
@@ -504,8 +497,8 @@ export class KaravanApi {
         });
     }
 
-    static async deleteContainer(environment: string, type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown', name: string, after: (res: AxiosResponse<any>) => void) {
-        instance.delete('/api/infrastructure/container/' + environment + '/' + type + "/" + name)
+    static async deleteContainer(environment: string, type: 'devmove' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown', name: string, after: (res: AxiosResponse<any>) => void) {
+        instance.delete('/api/container/' + environment + '/' + type + "/" + name)
             .then(res => {
                 after(res);
             }).catch(err => {
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
index 460690b7..8dd5e485 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
@@ -73,7 +73,7 @@ export class ContainerStatus {
     deployment: string = '';
     projectId: string = '';
     env: string = '';
-    type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown' = 'unknown';
+    type: 'devmove' | 'devservice' | 'project' | 'internal' | 'build' | 'unknown' = 'unknown';
     memoryInfo: string = '';
     cpuInfo: string = '';
     created: string = '';
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx b/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
index 1fa7a8f0..e18284c5 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/BuildToolbar.tsx
@@ -1,9 +1,18 @@
 import React from 'react';
-import {Button, Flex, FlexItem} from '@patternfly/react-core';
+import {Button, Flex, FlexItem, Label, Spinner, Tooltip, TooltipPosition} from '@patternfly/react-core';
 import '../designer/karavan.css';
 import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon";
-import {useAppConfigStore} from "../api/ProjectStore";
+import {useAppConfigStore, useDevModeStore, useLogStore, useProjectStore, useStatusesStore} from "../api/ProjectStore";
 import {shallow} from "zustand/shallow";
+import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
+import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
+import RunIcon from "@patternfly/react-icons/dist/esm/icons/play-icon";
+import {ProjectService} from "../api/ProjectService";
+import ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
+import {KaravanApi} from "../api/KaravanApi";
+import {ProjectEventBus} from "../api/ProjectEventBus";
+import {EventBus} from "../designer/utils/EventBus";
+import StopIcon from "@patternfly/react-icons/dist/js/icons/stop-icon";
 
 interface Props {
     reloadOnly?: boolean
@@ -12,11 +21,78 @@ interface Props {
 export function BuildToolbar (props: Props) {
 
     const [config] = useAppConfigStore((state) => [state.config], shallow)
+    const [status] = useDevModeStore((state) => [state.status], shallow)
+    const [project] = useProjectStore((state) => [state.project], shallow)
+    const [containers] = useStatusesStore((state) => [state.containers], shallow);
+    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
+
+    const containerStatus = containers.filter(c => c.containerName === project.projectId).at(0);
+    const commands = containerStatus?.commands || ['run'];
+    const isRunning = containerStatus?.state === 'running';
+    const inTransit = containerStatus?.inTransit;
+    const isLoading = status === 'wip';
+    const color = containerStatus?.state === 'running' ? "green" : "grey";
+    const icon = isRunning ? <UpIcon/> : <DownIcon/>;
 
     return (<Flex className="toolbar" direction={{default: "row"}} alignItems={{default: "alignItemsCenter"}}>
         <FlexItem>
-                <Button style={{visibility:"hidden"}} size="sm" variant={"control"} icon={<DeleteIcon/>} onClick={() => {}}>
+            {(inTransit || isLoading) && <Spinner size="lg" aria-label="spinner"/>}
+        </FlexItem>
+        {containerStatus?.containerId && <FlexItem>
+            <Label icon={icon} color={color}>
+                <Tooltip content={"Show log"} position={TooltipPosition.bottom}>
+                    <Button className='labeled-button' variant="link" isDisabled={!isRunning}
+                            onClick={e =>
+                                setShowLog(true, 'container', containerStatus.containerName)}>
+                        {containerStatus.containerName}
+                    </Button>
+                </Tooltip>
+            </Label>
+        </FlexItem>}
+        {!isRunning && <FlexItem>
+            <Tooltip content="Run container" position={TooltipPosition.bottom}>
+                <Button size="sm"
+                        isDisabled={(!(commands.length === 0) && !commands.includes('run')) || inTransit}
+                        variant={"primary"}
+                        icon={<RunIcon/>}
+                        onClick={() => {
+                            KaravanApi.manageContainer('dev', 'project', project.projectId, 'run', res => {
+                                setShowLog(false, 'container', undefined)
+                            });
+                        }}>
+                    {"Run"}
                 </Button>
+            </Tooltip>
+        </FlexItem>}
+        {config.infrastructure !== 'kubernetes' &&
+            <FlexItem>
+                <Tooltip content="Stop container" position={TooltipPosition.bottom}>
+                    <Button size="sm"
+                            isDisabled={!commands.includes('stop') || inTransit}
+                            variant={"control"}
+                            icon={<StopIcon/>}
+                            onClick={() => {
+                                KaravanApi.manageContainer('dev', 'project', project.projectId, 'stop', res => {
+                                    setShowLog(false, 'container', undefined)
+                                });
+                            }}>
+                    </Button>
+                </Tooltip>
+            </FlexItem>
+        }
+        <FlexItem>
+            <Tooltip content="Delete container" position={TooltipPosition.bottom}>
+                <Button size="sm"
+                        isDisabled={!commands.includes('delete') || inTransit}
+                        variant={"control"}
+                        icon={<DeleteIcon/>}
+                        onClick={() => {
+                            KaravanApi.manageContainer('dev', 'project', project.projectId, 'delete', res => {
+                                setShowLog(false, 'container', undefined)
+                            });
+                        }}>
+                </Button>
+            </Tooltip>
         </FlexItem>
     </Flex>);
-}
+}
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
index 6c916da4..e761bbcb 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/ProjectBuildTab.tsx
@@ -14,7 +14,7 @@ export function ProjectBuildTab () {
             <div>
                 {/*{["dev", "test", "prod"].map(env =>*/}
                 {config.environments.map(env =>
-                    <BuildPanel env={env}/>
+                    <BuildPanel key={env} env={env}/>
                 )}
             </div>
         </PageSection>
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceModal.tsx b/karavan-web/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceModal.tsx
index 6a2b9986..af950b60 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceModal.tsx
@@ -42,7 +42,6 @@ export function RunnerInfoTraceModal (props: Props) {
         return Array.from(new Set((props.nodes).map((item: any) => item?.routeId)));
     }
 
-    console.log(props.nodes)
     return (
         <Modal
             title={"Exchange: " + props.exchangeId}