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

[camel-karavan] 01/07: UI redesign #817

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

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

commit 396de51a5f17df294d2bb6e87b023885b7cac2c6
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Wed Jul 12 13:59:23 2023 -0400

    UI redesign #817
---
 ...DevModeService.java => CamelStatusService.java} |   0
 .../src/main/webui/src/api/KaravanApi.tsx          |  10 +-
 .../{RunnerToolbar.tsx => DevModeToolbar.tsx}      |   6 +-
 .../src/main/webui/src/project/ProjectPage.tsx     |   4 +-
 .../src/main/webui/src/project/ProjectToolbar.tsx  | 107 +--------------------
 .../webui/src/project/dashboard/DashboardTab.tsx   |  29 +++---
 .../{RunnerInfoContext.tsx => InfoContext.tsx}     |   2 +-
 .../{RunnerInfoMemory.tsx => InfoMemory.tsx}       |   2 +-
 .../dashboard/{RunnerInfoPod.tsx => InfoPod.tsx}   |  59 +++---------
 .../main/webui/src/project/files/FilesToolbar.tsx  | 101 ++++++++++++++++++-
 .../webui/src/project/pipeline/ProjectStatus.tsx   |   6 +-
 .../src/main/webui/src/project/trace/TraceTab.tsx  |   2 +-
 12 files changed, 144 insertions(+), 184 deletions(-)

diff --git a/karavan-cloud/karavan-app/src/main/java/org/apache/camel/karavan/service/DevModeService.java b/karavan-cloud/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelStatusService.java
similarity index 100%
rename from karavan-cloud/karavan-app/src/main/java/org/apache/camel/karavan/service/DevModeService.java
rename to karavan-cloud/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelStatusService.java
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-cloud/karavan-app/src/main/webui/src/api/KaravanApi.tsx
index c2b53c14..efb86595 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/api/KaravanApi.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/api/KaravanApi.tsx
@@ -303,16 +303,17 @@ export class KaravanApi {
         });
     }
 
-    static async getRunnerPodStatus(projectId: string, after: (res: AxiosResponse<PodStatus>) => void) {
+    static async getDevModePodStatus(projectId: string, after: (res: AxiosResponse<PodStatus>) => void) {
         instance.get('/api/devmode/pod/' + projectId)
             .then(res => {
+                console.log(res);
                 after(res);
             }).catch(err => {
             after(err);
         });
     }
 
-    static async getRunnerReload(projectId: string, after: (res: AxiosResponse<any>) => void) {
+    static async reloadDevMode(projectId: string, after: (res: AxiosResponse<any>) => void) {
         instance.get('/api/devmode/reload/' + projectId)
             .then(res => {
                 after(res);
@@ -321,9 +322,10 @@ export class KaravanApi {
         });
     }
 
-    static async getRunnerConsoleStatus(projectId: string, statusName: string, after: (res: AxiosResponse<string>) => void) {
-        instance.get('/api/devmode/console/' + projectId + "/" + statusName)
+    static async getDevModeStatus(projectId: string, statusName: string, after: (res: AxiosResponse<string>) => void) {
+        instance.get('/api/devmode/status/' + projectId + "/" + statusName)
             .then(res => {
+                console.log(res);
                 after(res);
             }).catch(err => {
             after(err);
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
similarity index 93%
rename from karavan-cloud/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx
rename to karavan-cloud/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
index a065773c..fc099b50 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/DevModeToolbar.tsx
@@ -17,7 +17,7 @@ interface Props {
     reloadOnly?: boolean
 }
 
-export const RunnerToolbar = (props: Props) => {
+export const DevModeToolbar = (props: Props) => {
 
     const [status] = useRunnerStore((state) => [state.status], shallow)
     const [project] = useProjectStore((state) => [state.project], shallow)
@@ -29,7 +29,7 @@ export const RunnerToolbar = (props: Props) => {
     const isDeletingPod = status === "deleting";
     return (<>
         {(isRunning || isDeletingPod) && !isReloadingPod && props.reloadOnly !== true && <FlexItem>
-            <Tooltip content="Stop runner" position={TooltipPosition.bottom}>
+            <Tooltip content="Stop devmode" position={TooltipPosition.bottom}>
                 <Button isLoading={isDeletingPod ? true : undefined}
                         isSmall
                         variant={"secondary"}
@@ -50,7 +50,7 @@ export const RunnerToolbar = (props: Props) => {
             </Tooltip>
         </FlexItem>}
         {!isRunning && !isReloadingPod && props.reloadOnly !== true && <FlexItem>
-            <Tooltip content="Run in development mode" position={TooltipPosition.bottom}>
+            <Tooltip content="Run in developer mode" position={TooltipPosition.bottom}>
                 <Button isLoading={isStartingPod ? true : undefined}
                         isSmall
                         variant={"primary"}
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/ProjectPage.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/ProjectPage.tsx
index 1221e0cb..6ad2bdd9 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/ProjectPage.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/ProjectPage.tsx
@@ -27,8 +27,8 @@ export const ProjectPage = () => {
     useEffect(() => {
         // TODO: make status request only when started or just opened
         const interval = setInterval(() => {
-            ProjectService.getRunnerPodStatus(project);
-        }, 1000);
+            ProjectService.getDevModePodStatus(project);
+        }, 2000);
         return () => {
             clearInterval(interval)
         };
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
index 95d177ae..c65d5ced 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
@@ -22,9 +22,8 @@ import '../designer/karavan.css';
 import DownloadImageIcon from "@patternfly/react-icons/dist/esm/icons/image-icon";
 import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon";
 import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml";
-import PushIcon from "@patternfly/react-icons/dist/esm/icons/code-branch-icon";
-import {RunnerToolbar} from "./RunnerToolbar";
-import {useFilesStore, useFileStore, useProjectStore} from "../api/ProjectStore";
+import {DevModeToolbar} from "./DevModeToolbar";
+import {useFileStore, useProjectStore} from "../api/ProjectStore";
 import {EventBus} from "../designer/utils/EventBus";
 import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
@@ -38,10 +37,7 @@ interface Props {
 
 export const ProjectToolbar = (props: Props) => {
 
-    const [commitMessageIsOpen, setCommitMessageIsOpen] = useState(false);
-    const [commitMessage, setCommitMessage] = useState('');
     const [project, isPushing] = useProjectStore((state) => [state.project, state.isPushing], shallow )
-    const {files} = useFilesStore();
     const [file, editAdvancedProperties, setEditAdvancedProperties, setAddProperty] = useFileStore((state) =>
         [state.file, state.editAdvancedProperties, state.setEditAdvancedProperties, state.setAddProperty], shallow )
 
@@ -69,10 +65,6 @@ export const ProjectToolbar = (props: Props) => {
         return file !== undefined && file.name.endsWith("java");
     }
 
-    function needCommit(): boolean {
-        return project ? files.filter(f => f.lastUpdate > project.lastCommitTimestamp).length > 0 : false;
-    }
-
     function downloadImage () {
         EventBus.sendCommand("downloadImage");
     }
@@ -88,43 +80,6 @@ export const ProjectToolbar = (props: Props) => {
         }
     }
 
-    function push () {
-        setCommitMessageIsOpen(false);
-        ProjectService.pushProject(project, commitMessage);
-    }
-
-    function getDate(lastUpdate: number): string {
-        if (lastUpdate) {
-            const date = new Date(lastUpdate);
-            return date.toISOString().slice(0, 19).replace('T',' ');
-        } else {
-            return "N/A"
-        }
-    }
-
-    function getLastUpdatePanel() {
-        const color = needCommit() ? "grey" : "green";
-        const commit = project?.lastCommit;
-        return (
-            <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexStart"}}>
-                {project?.lastCommitTimestamp > 0 &&
-                    <FlexItem>
-                        <Tooltip content="Last update" position={TooltipPosition.bottom}>
-                            <Label color={color}>{getDate(project?.lastCommitTimestamp)}</Label>
-                        </Tooltip>
-                    </FlexItem>}
-                {project?.lastCommitTimestamp > 0 &&
-                <FlexItem>
-                    <Tooltip content={commit} position={TooltipPosition.bottom}>
-                        <Label
-                            color={color}>{commit ? commit?.substring(0, 18) : "-"}</Label>
-                    </Tooltip>
-                </FlexItem>}
-            </Flex>
-        )
-    }
-
-
     function getFileToolbar() {
         const { mode} = props;
         return <Toolbar id="toolbar-group-types">
@@ -135,22 +90,7 @@ export const ProjectToolbar = (props: Props) => {
                             <Label>{file?.code?.length}</Label>
                         </Tooltip>
                     </FlexItem>}
-                    {isRunnable() && <RunnerToolbar reloadOnly={true}/>}
-                    {!isFile && <FlexItem>
-                        <Tooltip content="Commit and push to git" position={"bottom-end"}>
-                            <Button isLoading={isPushing ? true : undefined}
-                                    isSmall
-                                    variant={needCommit() ? "primary" : "secondary"}
-                                    className="project-button"
-                                    icon={!isPushing ? <PushIcon/> : <div></div>}
-                                    onClick={() => {
-                                        setCommitMessage(commitMessage === '' ? new Date().toLocaleString() : commitMessage);
-                                        setCommitMessageIsOpen(true);
-                                    }}>
-                                {isPushing ? "..." : "Push"}
-                            </Button>
-                        </Tooltip>
-                    </FlexItem>}
+                    {isRunnable() && <DevModeToolbar reloadOnly={true}/>}
                     {isYaml() && <FlexItem>
                         <ToggleGroup>
                             <ToggleGroupItem text="Design" buttonId="design" isSelected={mode === "design"}
@@ -186,49 +126,13 @@ export const ProjectToolbar = (props: Props) => {
         return (<Toolbar id="toolbar-group-types">
             <ToolbarContent>
                 <Flex className="toolbar" direction={{default: "row"}} alignItems={{default: "alignItemsCenter"}}>
-                    <FlexItem>{getLastUpdatePanel()}</FlexItem>
-                    {isRunnable() && <RunnerToolbar/>}
-                    <FlexItem>
-                        <Tooltip content="Commit and push to git" position={"bottom-end"}>
-                            <Button isLoading={isPushing ? true : undefined}
-                                    isSmall
-                                    variant={needCommit() ? "primary" : "secondary"}
-                                    className="project-button"
-                                    icon={!isPushing ? <PushIcon/> : <div></div>}
-                                    onClick={() => {
-                                        setCommitMessage(commitMessage === '' ? new Date().toLocaleString() : commitMessage);
-                                        setCommitMessageIsOpen(true);
-                                    }}>
-                                {isPushing ? "..." : "Push"}
-                            </Button>
-                        </Tooltip>
-                    </FlexItem>
+                    {isRunnable() && <DevModeToolbar/>}
                 </Flex>
             </ToolbarContent>
         </Toolbar>)
     }
 
-    function getCommitModal() {
-        return (
-            <Modal
-                title="Commit"
-                variant={ModalVariant.small}
-                isOpen={commitMessageIsOpen}
-                onClose={() => setCommitMessageIsOpen(false)}
-                actions={[
-                    <Button key="confirm" variant="primary" onClick={() => push()}>Save</Button>,
-                    <Button key="cancel" variant="secondary" onClick={() => setCommitMessageIsOpen(false)}>Cancel</Button>
-                ]}
-            >
-                <Form autoComplete="off" isHorizontal className="create-file-form">
-                    <FormGroup label="Message" fieldId="name" isRequired>
-                        <TextInput value={commitMessage} onChange={value => setCommitMessage(value)}/>
-                        <FormHelperText isHidden={false} component="div"/>
-                    </FormGroup>
-                </Form>
-            </Modal>
-        )
-    }
+
 
     function isKameletsProject(): boolean {
         return project.projectId === 'kamelets';
@@ -249,7 +153,6 @@ export const ProjectToolbar = (props: Props) => {
             {/*{!isTemplates && getProjectToolbar()}*/}
              {!isFile() && getProjectToolbar()}
              {isFile() && getFileToolbar()}
-             {getCommitModal()}
         </>
     )
 }
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
index ee0135bf..a2b90e28 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/DashboardTab.tsx
@@ -14,23 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useEffect, useState} from 'react';
 import {
     Card,
     CardBody, Flex, FlexItem, Divider, PageSection
 } from '@patternfly/react-core';
 import '../../designer/karavan.css';
-import {RunnerInfoPod} from "./RunnerInfoPod";
-import {RunnerInfoContext} from "./RunnerInfoContext";
-import {RunnerInfoMemory} from "./RunnerInfoMemory";
+import {InfoPod} from "./InfoPod";
+import {InfoContext} from "./InfoContext";
+import {InfoMemory} from "./InfoMemory";
 import {KaravanApi} from "../../api/KaravanApi";
-import {PodStatus} from "../../api/ProjectModels";
 import {useProjectStore} from "../../api/ProjectStore";
 
-export function isRunning(status: PodStatus): boolean {
-    return status.phase === 'Running' && !status.terminating;
-}
-
 export const DashboardTab = () => {
 
     const {project, podStatus} = useProjectStore();
@@ -41,7 +36,7 @@ export const DashboardTab = () => {
     useEffect(() => {
         const interval = setInterval(() => {
             onRefreshStatus();
-        }, 1000);
+        }, 2000);
         return () => {
             clearInterval(interval)
         };
@@ -50,21 +45,21 @@ export const DashboardTab = () => {
 
     function onRefreshStatus() {
         const projectId = project.projectId;
-        KaravanApi.getRunnerConsoleStatus(projectId, "memory", res => {
+        KaravanApi.getDevModeStatus(projectId, "memory", res => {
             if (res.status === 200) {
                 setMemory(res.data);
             } else {
                 setMemory({});
             }
         })
-        KaravanApi.getRunnerConsoleStatus(projectId, "jvm", res => {
+        KaravanApi.getDevModeStatus(projectId, "jvm", res => {
             if (res.status === 200) {
                 setJvm(res.data);
             } else {
                 setJvm({});
             }
         })
-        KaravanApi.getRunnerConsoleStatus(projectId, "context", res => {
+        KaravanApi.getDevModeStatus(projectId, "context", res => {
             if (res.status === 200) {
                 setContext(res.data);
             } else {
@@ -74,7 +69,7 @@ export const DashboardTab = () => {
     }
 
     function showConsole(): boolean {
-        return podStatus.phase !== '';
+        return podStatus.ready;
     }
 
     return (
@@ -84,15 +79,15 @@ export const DashboardTab = () => {
                     <Flex direction={{default: "row"}}
                           justifyContent={{default: "justifyContentSpaceBetween"}}>
                         <FlexItem flex={{default: "flex_1"}}>
-                            <RunnerInfoPod podStatus={podStatus}/>
+                            <InfoPod podStatus={podStatus}/>
                         </FlexItem>
                         <Divider orientation={{default: "vertical"}}/>
                         <FlexItem flex={{default: "flex_1"}}>
-                            <RunnerInfoMemory jvm={jvm} memory={memory} showConsole={showConsole()}/>
+                            <InfoMemory jvm={jvm} memory={memory} showConsole={showConsole()}/>
                         </FlexItem>
                         <Divider orientation={{default: "vertical"}}/>
                         <FlexItem flex={{default: "flex_1"}}>
-                            <RunnerInfoContext context={context} showConsole={showConsole()}/>
+                            <InfoContext context={context} showConsole={showConsole()}/>
                         </FlexItem>
                     </Flex>
                 </CardBody>
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoContext.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoContext.tsx
similarity index 99%
rename from karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoContext.tsx
rename to karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoContext.tsx
index c07cf810..69134d23 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoContext.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoContext.tsx
@@ -17,7 +17,7 @@ interface Props {
     showConsole: boolean
 }
 
-export const RunnerInfoContext = (props: Props) => {
+export const InfoContext = (props: Props) => {
 
     function getContextInfo() {
         return (
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoMemory.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoMemory.tsx
similarity index 98%
rename from karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoMemory.tsx
rename to karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoMemory.tsx
index 67e576ad..28aee4ac 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoMemory.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoMemory.tsx
@@ -18,7 +18,7 @@ interface Props {
     showConsole: boolean
 }
 
-export const RunnerInfoMemory = (props: Props) => {
+export const InfoMemory = (props: Props) => {
 
     function getJvmInfo() {
         return (
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoPod.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoPod.tsx
similarity index 59%
rename from karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoPod.tsx
rename to karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoPod.tsx
index 6dbda49c..af24bde5 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/RunnerInfoPod.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/dashboard/InfoPod.tsx
@@ -12,18 +12,14 @@ import '../../designer/karavan.css';
 import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
 import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
 import {PodStatus} from "../../api/ProjectModels";
-import {useLogStore, useRunnerStore} from "../../api/ProjectStore";
+import {useLogStore} from "../../api/ProjectStore";
 
 
-export function isRunning(status: PodStatus): boolean {
-    return status.phase === 'Running' && !status.terminating;
-}
-
 interface Props {
     podStatus: PodStatus,
 }
 
-export const RunnerInfoPod = (props: Props) => {
+export const InfoPod = (props: Props) => {
 
     function getPodInfo() {
         const podStatus = props.podStatus;
@@ -40,42 +36,10 @@ export const RunnerInfoPod = (props: Props) => {
         )
     }
 
-    function getPodStatus() {
-        const podStatus = props.podStatus;
-        const status = !podStatus.terminating ? podStatus.phase : "Terminating"
-        return (
-            <Label icon={getIcon()} color={getColor()}>
-                {status !== "" ? status : "N/A"}
-            </Label>
-        )
-    }
-
-    function getPodRequests() {
-        const podStatus = props.podStatus;
-        const text = podStatus.requestCpu !== '' ? podStatus.requestCpu + " : " + podStatus.requestMemory : "N/A";
-        return (
-            <Label icon={getIcon()} color={getColor()}>
-                {text}
-            </Label>
-        )
-    }
-
-    function getPodCreation() {
-        const podStatus = props.podStatus;
-        const text = podStatus.creationTimestamp !== '' ? podStatus.creationTimestamp : "N/A";
-        return (
-            <Label icon={getIcon()} color={getColor()}>
-                {text}
-            </Label>
-        )
-    }
-
-    function getPodLimits() {
-        const podStatus = props.podStatus;
-        const text = podStatus.limitCpu !== '' ? podStatus.limitCpu + " : " + podStatus.limitMemory : "N/A";
+    function getPodInfoLabel(info: string) {
         return (
             <Label icon={getIcon()} color={getColor()}>
-                {text}
+                {info}
             </Label>
         )
     }
@@ -89,9 +53,10 @@ export const RunnerInfoPod = (props: Props) => {
     }
 
     function getRunning(): boolean {
-        return isRunning(props.podStatus);
+        return props.podStatus.ready;
     }
 
+    const podStatus = props.podStatus;
     return (
         <DescriptionList isHorizontal>
             <DescriptionListGroup>
@@ -103,25 +68,25 @@ export const RunnerInfoPod = (props: Props) => {
             <DescriptionListGroup>
                 <DescriptionListTerm>Status</DescriptionListTerm>
                 <DescriptionListDescription>
-                    {getPodStatus()}
+                    {getPodInfoLabel(podStatus.ready ? "Ready" : "Not Ready")}
                 </DescriptionListDescription>
             </DescriptionListGroup>
             <DescriptionListGroup>
-                <DescriptionListTerm>Requests</DescriptionListTerm>
+                <DescriptionListTerm>CPU</DescriptionListTerm>
                 <DescriptionListDescription>
-                    {getPodRequests()}
+                    {getPodInfoLabel(podStatus.cpuInfo)}
                 </DescriptionListDescription>
             </DescriptionListGroup>
             <DescriptionListGroup>
-                <DescriptionListTerm>Limits</DescriptionListTerm>
+                <DescriptionListTerm>Memory</DescriptionListTerm>
                 <DescriptionListDescription>
-                    {getPodLimits()}
+                    {getPodInfoLabel(podStatus.memoryInfo)}
                 </DescriptionListDescription>
             </DescriptionListGroup>
             <DescriptionListGroup>
                 <DescriptionListTerm>Created</DescriptionListTerm>
                 <DescriptionListDescription>
-                    {getPodCreation()}
+                    {getPodInfoLabel(podStatus.created)}
                 </DescriptionListDescription>
             </DescriptionListGroup>
         </DescriptionList>
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/files/FilesToolbar.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/files/FilesToolbar.tsx
index 00835fe4..d35ed094 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/files/FilesToolbar.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/files/FilesToolbar.tsx
@@ -14,20 +14,114 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React from 'react';
+import React, {useEffect, useState} from 'react';
 import {
     Button,
     Flex,
-    FlexItem,
+    FlexItem, Form, FormGroup, FormHelperText, Label, Modal, ModalVariant, TextInput, Tooltip, TooltipPosition,
 } from '@patternfly/react-core';
 import '../../designer/karavan.css';
 import UploadIcon from "@patternfly/react-icons/dist/esm/icons/upload-icon";
 import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon";
-import {useFileStore} from "../../api/ProjectStore";
+import {useFilesStore, useFileStore, useProjectStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {ProjectService} from "../../api/ProjectService";
+import PushIcon from "@patternfly/react-icons/dist/esm/icons/code-branch-icon";
 
 export const FileToolbar = () => {
 
+    const [commitMessageIsOpen, setCommitMessageIsOpen] = useState(false);
+    const [commitMessage, setCommitMessage] = useState('');
+    const [project, isPushing] = useProjectStore((state) => [state.project, state.isPushing], shallow )
+    const {files} = useFilesStore();
+    const [file, editAdvancedProperties, setEditAdvancedProperties, setAddProperty] = useFileStore((state) =>
+        [state.file, state.editAdvancedProperties, state.setEditAdvancedProperties, state.setAddProperty], shallow )
+
+
+    useEffect(() => {
+        console.log("ProjectToolbar useEffect", isPushing, project.lastCommitTimestamp);
+    }, [project, file]);
+
+    function push () {
+        setCommitMessageIsOpen(false);
+        ProjectService.pushProject(project, commitMessage);
+    }
+
+    function getCommitModal() {
+        return (
+            <Modal
+                title="Commit"
+                variant={ModalVariant.small}
+                isOpen={commitMessageIsOpen}
+                onClose={() => setCommitMessageIsOpen(false)}
+                actions={[
+                    <Button key="confirm" variant="primary" onClick={() => push()}>Save</Button>,
+                    <Button key="cancel" variant="secondary" onClick={() => setCommitMessageIsOpen(false)}>Cancel</Button>
+                ]}
+            >
+                <Form autoComplete="off" isHorizontal className="create-file-form">
+                    <FormGroup label="Message" fieldId="name" isRequired>
+                        <TextInput value={commitMessage} onChange={value => setCommitMessage(value)}/>
+                        <FormHelperText isHidden={false} component="div"/>
+                    </FormGroup>
+                </Form>
+            </Modal>
+        )
+    }
+
+    function needCommit(): boolean {
+        return project ? files.filter(f => f.lastUpdate > project.lastCommitTimestamp).length > 0 : false;
+    }
+
+
+    function getDate(lastUpdate: number): string {
+        if (lastUpdate) {
+            const date = new Date(lastUpdate);
+            return date.toISOString().slice(0, 19).replace('T',' ');
+        } else {
+            return "N/A"
+        }
+    }
+
+    function getLastUpdatePanel() {
+        const color = needCommit() ? "grey" : "green";
+        const commit = project?.lastCommit;
+        return (
+            <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexStart"}}>
+                {project?.lastCommitTimestamp > 0 &&
+                    <FlexItem>
+                        <Tooltip content="Last update" position={TooltipPosition.bottom}>
+                            <Label color={color}>{getDate(project?.lastCommitTimestamp)}</Label>
+                        </Tooltip>
+                    </FlexItem>}
+                {project?.lastCommitTimestamp > 0 &&
+                    <FlexItem>
+                        <Tooltip content={commit} position={TooltipPosition.bottom}>
+                            <Label
+                                color={color}>{commit ? commit?.substring(0, 18) : "-"}</Label>
+                        </Tooltip>
+                    </FlexItem>}
+            </Flex>
+        )
+    }
+
     return <Flex className="toolbar" direction={{default: "row"}} justifyContent={{default: "justifyContentFlexEnd"}}>
+        <FlexItem>{getLastUpdatePanel()}</FlexItem>
+        <FlexItem>
+            <Tooltip content="Commit and push to git" position={"bottom-end"}>
+                <Button isLoading={isPushing ? true : undefined}
+                        isSmall
+                        variant={needCommit() ? "primary" : "secondary"}
+                        className="project-button"
+                        icon={!isPushing ? <PushIcon/> : <div></div>}
+                        onClick={() => {
+                            setCommitMessage(commitMessage === '' ? new Date().toLocaleString() : commitMessage);
+                            setCommitMessageIsOpen(true);
+                        }}>
+                    {isPushing ? "..." : "Push"}
+                </Button>
+            </Tooltip>
+        </FlexItem>
         <FlexItem>
             <Button isSmall variant={"secondary"} icon={<PlusIcon/>}
                     onClick={e => useFileStore.setState({operation:"create"})}>Create</Button>
@@ -36,5 +130,6 @@ export const FileToolbar = () => {
             <Button isSmall variant="secondary" icon={<UploadIcon/>}
                     onClick={e => useFileStore.setState({operation:"upload"})}>Upload</Button>
         </FlexItem>
+        {getCommitModal()}
     </Flex>
 }
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
index 67ec90ea..25ac03f9 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx
@@ -201,10 +201,10 @@ export class ProjectStatus extends React.Component<Props, State> {
                     {podStatuses.length === 0 && <Label icon={<DownIcon/>} color={"grey"}>No pods</Label>}
                     <LabelGroup numLabels={2} isVertical>
                         {podStatuses.map(pod => {
-                                const running = pod.phase === 'Running' && !pod.terminating
+                                const ready = pod.ready;
                                 return (
-                                    <Tooltip key={pod.name} content={running ? "Running" : pod.phase}>
-                                        <Label icon={running ? <UpIcon/> : <DownIcon/>} color={running ? "green" : "red"}>
+                                    <Tooltip key={pod.name} content={ready ? "Ready" : "Not ready"}>
+                                        <Label icon={ready ? <UpIcon/> : <DownIcon/>} color={ready ? "green" : "red"}>
                                             <Button variant="link"
                                                     onClick={e => {
                                                         useLogStore.setState({
diff --git a/karavan-cloud/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx b/karavan-cloud/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx
index a33eb340..aa076f81 100644
--- a/karavan-cloud/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx
+++ b/karavan-cloud/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx
@@ -55,7 +55,7 @@ export const TraceTab = () => {
     function onRefreshStatus() {
         const projectId = project.projectId;
         if (refreshTrace) {
-            KaravanApi.getRunnerConsoleStatus(projectId, "trace", res => {
+            KaravanApi.getDevModeStatus(projectId, "trace", res => {
                 if (res.status === 200) {
                     setTrace(res.data);
                 } else {