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/02 21:22:03 UTC

[camel-karavan] 03/03: Refactoring for properties #809

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 8830307331058de52c13071f7fc8ef5192fc3eec
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Sun Jul 2 17:21:49 2023 -0400

    Refactoring for properties #809
---
 .../src/main/webui/src/api/ProjectService.ts       |  6 +-
 karavan-app/src/main/webui/src/api/ProjectStore.ts | 45 +++++++----
 karavan-app/src/main/webui/src/index.css           |  3 +
 .../src/main/webui/src/project/ProjectPage.tsx     |  7 +-
 .../src/main/webui/src/project/ProjectToolbar.tsx  | 93 +++++++++++-----------
 .../src/main/webui/src/project/RunnerToolbar.tsx   | 15 ++--
 .../src/main/webui/src/project/file/FileEditor.tsx | 28 -------
 .../webui/src/project/file/PropertiesTable.tsx     | 46 ++++-------
 .../main/webui/src/project/file/PropertyField.tsx  | 74 +++++++++++++++++
 .../src/main/webui/src/project/log/ProjectLog.tsx  | 15 +++-
 10 files changed, 198 insertions(+), 134 deletions(-)

diff --git a/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-app/src/main/webui/src/api/ProjectService.ts
index 97c467c9..f4bb71ec 100644
--- a/karavan-app/src/main/webui/src/api/ProjectService.ts
+++ b/karavan-app/src/main/webui/src/api/ProjectService.ts
@@ -67,8 +67,10 @@ export class ProjectService {
                 })
             } else {
                 unstable_batchedUpdates(() => {
-                    useRunnerStore.setState({status: "none", podName: undefined})
-                    useProjectStore.setState({podStatus: new PodStatus()});
+                    if (useRunnerStore.getState().status !== 'none') {
+                        useRunnerStore.setState({status: "none", podName: undefined})
+                        useProjectStore.setState({podStatus: new PodStatus()});
+                    }
                 })
             }
         });
diff --git a/karavan-app/src/main/webui/src/api/ProjectStore.ts b/karavan-app/src/main/webui/src/api/ProjectStore.ts
index 265c9717..89c6e77e 100644
--- a/karavan-app/src/main/webui/src/api/ProjectStore.ts
+++ b/karavan-app/src/main/webui/src/api/ProjectStore.ts
@@ -19,6 +19,7 @@ import {create} from 'zustand'
 import {AppConfig, DeploymentStatus, PodStatus, Project, ProjectFile, ToastMessage} from "./ProjectModels";
 import {ProjectEventBus} from "./ProjectEventBus";
 import {unstable_batchedUpdates} from "react-dom";
+import {bottom} from "@patternfly/react-core/helpers/Popper/thirdparty/popper-core";
 
 interface AppConfigState {
     config: AppConfig;
@@ -28,7 +29,7 @@ interface AppConfigState {
 export const useAppConfigStore = create<AppConfigState>((set) => ({
     config: new AppConfig(),
     setConfig: (config: AppConfig)  => {
-        set({config: config}, true)
+        set({config: config})
     },
 }))
 
@@ -44,14 +45,14 @@ export const useProjectsStore = create<ProjectsState>((set) => ({
     setProjects: (ps: Project[]) => {
         set((state: ProjectsState) => ({
             projects: ps,
-        }), true);
+        }));
     },
     upsertProject: (project: Project) => {
         set((state: ProjectsState) => ({
             projects: state.projects.find(f => f.projectId === project.projectId) === undefined
                 ? [...state.projects, project]
                 : [...state.projects.filter(f => f.projectId !== project.projectId), project]
-        }), true);
+        }));
     }
 }))
 
@@ -74,12 +75,12 @@ export const useProjectStore = create<ProjectState>((set) => ({
     setProject: (p: Project) => {
         set((state: ProjectState) => ({
             project: p
-        }), true);
+        }));
     },
     setOperation: (o: "create" | "select" | "delete"| "none" | "copy") => {
         set((state: ProjectState) => ({
             operation: o
-        }), true);
+        }));
     },
 }))
 
@@ -94,14 +95,14 @@ export const useFilesStore = create<FilesState>((set) => ({
     setFiles: (files: ProjectFile[]) => {
         set((state: FilesState) => ({
             files: files
-        }), true);
+        }));
     },
     upsertFile: (file: ProjectFile) => {
         set((state: FilesState) => ({
             files: state.files.find(f => f.name === file.name) === undefined
                 ? [...state.files, file]
                 : [...state.files.filter(f => f.name !== file.name), file]
-        }), true);
+        }));
     }
 }))
 
@@ -109,16 +110,28 @@ interface FileState {
     file?: ProjectFile;
     operation: "create" | "select" | "delete" | "none" | "copy" | "upload";
     setFile: (file: ProjectFile, operation:  "create" | "select" | "delete"| "none" | "copy" | "upload") => void;
+    editAdvancedProperties: boolean;
+    setEditAdvancedProperties: (editAdvancedProperties: boolean) => void;
+    addProperty: string;
+    setAddProperty: (addProperty: string) => void;
 }
 
 export const useFileStore = create<FileState>((set) => ({
     file: undefined,
     operation: "none",
+    editAdvancedProperties: false,
+    addProperty: '',
     setFile: (file: ProjectFile, operation:  "create" | "select" | "delete"| "none" | "copy" | "upload") => {
         set((state: FileState) => ({
             file: file,
             operation: operation
-        }), true);
+        }));
+    },
+    setEditAdvancedProperties: (editAdvancedProperties: boolean) => {
+        set(() => ({editAdvancedProperties: editAdvancedProperties}));
+    },
+    setAddProperty: (addProperty: string) => {
+        set(() => ({addProperty: addProperty}));
     },
 }))
 
@@ -132,7 +145,7 @@ export const useDeploymentStatusesStore = create<DeploymentStatusesState>((set)
     setDeploymentStatuses: (statuses: DeploymentStatus[]) => {
         set((state: DeploymentStatusesState) => ({
             statuses: statuses
-        }), true);
+        }));
     },
 }))
 
@@ -149,7 +162,7 @@ export const useRunnerStore = create<RunnerState>((set) => ({
     setStatus: (status: "none" | "starting" | "deleting"| "reloading" | "running") =>  {
         set((state: RunnerState) => ({
             status: status,
-        }), true);
+        }));
     },
 }))
 
@@ -171,25 +184,25 @@ export const useLogStore = create<LogState>((set) => ({
     podName: undefined,
     data: '',
     setData: (data: string)  => {
-        set({data: data}, true)
+        set({data: data})
     },
     addData: (data: string)  => {
-        set((state: LogState) => ({data: state.data.concat('\n').concat(data)}), true)
+        set((state: LogState) => ({data: state.data.concat('\n').concat(data)}))
     },
     addDataAsync: async (data: string) => {
-        set((state: LogState) => ({data: state.data.concat('\n').concat(data)}), true)
+        set((state: LogState) => ({data: state.data.concat('\n').concat(data)}))
     },
     currentLine: 0,
     setCurrentLine: (currentLine: number)  => {
-        set((state: LogState) => ({currentLine: currentLine}), true)
+        set((state: LogState) => ({currentLine: currentLine}))
     },
     showLog: false,
     setShowLog: (showLog: boolean) => {
-        set(() => ({showLog: showLog}), true);
+        set(() => ({showLog: showLog}));
     },
     type: "none",
     setType: (type: 'container' | 'pipeline' | 'none') =>  {
-        set((state: LogState) => ({type: type}), true);
+        set((state: LogState) => ({type: type}));
     },
 }))
 
diff --git a/karavan-app/src/main/webui/src/index.css b/karavan-app/src/main/webui/src/index.css
index 1baa8f91..54dbc583 100644
--- a/karavan-app/src/main/webui/src/index.css
+++ b/karavan-app/src/main/webui/src/index.css
@@ -186,6 +186,9 @@
   right: 0;
   width: 100%;
   z-index: 200;
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
 }
 
 .karavan .project-page .project-log .buttons {
diff --git a/karavan-app/src/main/webui/src/project/ProjectPage.tsx b/karavan-app/src/main/webui/src/project/ProjectPage.tsx
index 817b8980..1221e0cb 100644
--- a/karavan-app/src/main/webui/src/project/ProjectPage.tsx
+++ b/karavan-app/src/main/webui/src/project/ProjectPage.tsx
@@ -19,14 +19,13 @@ import {shallow} from "zustand/shallow";
 export const ProjectPage = () => {
 
     const [isUploadModalOpen, setIsUploadModalOpen] = useState<boolean>(false);
-    const [editAdvancedProperties, setEditAdvancedProperties] = useState<boolean>(false);
     const {file, operation} = useFileStore();
     const [mode, setMode] = useState<"design" | "code">("design");
     const [key, setKey] = useState<string>('');
     const [project] = useProjectStore((state) => [state.project], shallow )
 
     useEffect(() => {
-        console.log("Project page")
+        // TODO: make status request only when started or just opened
         const interval = setInterval(() => {
             ProjectService.getRunnerPodStatus(project);
         }, 1000);
@@ -70,12 +69,8 @@ export const ProjectPage = () => {
 
     function tools () {
         return <ProjectToolbar
-                               file={file}
                                mode={mode}
-                               editAdvancedProperties={editAdvancedProperties}
-                               setEditAdvancedProperties={checked => setEditAdvancedProperties(checked)}
                                setMode={mode => setMode(mode)}
-                               setUploadModalOpen={() => setIsUploadModalOpen(isUploadModalOpen)}
         />
     }
 
diff --git a/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx b/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
index c1312692..23802bca 100644
--- a/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
+++ b/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx
@@ -15,31 +15,24 @@ import {
     ToggleGroupItem,
     Toolbar,
     ToolbarContent,
-    ToolbarItem,
     Tooltip,
     TooltipPosition
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
-import UploadIcon from "@patternfly/react-icons/dist/esm/icons/upload-icon";
-import DownloadIcon from "@patternfly/react-icons/dist/esm/icons/download-icon";
 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 ReloadIcon from "@patternfly/react-icons/dist/esm/icons/bolt-icon";
 import {RunnerToolbar} from "./RunnerToolbar";
-import {ProjectFile} from "../api/ProjectModels";
-import {useFilesStore, useProjectStore, useRunnerStore} from "../api/ProjectStore";
+import {useFilesStore, useFileStore, useProjectStore} from "../api/ProjectStore";
 import {EventBus} from "../designer/utils/EventBus";
 import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
+import {ProjectModelApi} from "karavan-core/lib/api/ProjectModelApi";
+import {ProjectModel, ProjectProperty} from "karavan-core/lib/model/ProjectModel";
 
 interface Props {
-    file?: ProjectFile,
     mode: "design" | "code",
-    editAdvancedProperties: boolean,
-    setUploadModalOpen: () => void,
-    setEditAdvancedProperties: (checked: boolean) => void,
     setMode: (mode: "design" | "code") => void,
 }
 
@@ -47,26 +40,34 @@ export const ProjectToolbar = (props: Props) => {
 
     const [commitMessageIsOpen, setCommitMessageIsOpen] = useState(false);
     const [commitMessage, setCommitMessage] = useState('');
-    const [isFile, setIsFile] = useState(false);
-    const [isYaml, setIsYaml] = useState(false);
-    const [isIntegration, setIsIntegration] = useState(false);
-    const [isProperties, setIsProperties] = useState(false);
-    const [ project, isPushing] = useProjectStore((state) => [state.project, state.isPushing], shallow )
+    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);
-        const {file, mode, editAdvancedProperties,
-            setEditAdvancedProperties, setUploadModalOpen} = props;
-        const isFile = file !== undefined;
-        const isYaml = file !== undefined && file.name.endsWith("yaml");
-        const isIntegration = isYaml && file?.code !== undefined && CamelDefinitionYaml.yamlIsIntegration(file.code);
-        const isProperties = file !== undefined && file.name.endsWith("properties");
-        setIsFile(isFile);
-        setIsYaml(isYaml);
-        setIsIntegration(isIntegration);
-        setIsProperties(isProperties);
-    }, [project]);
+    }, [project, file]);
+
+    function isFile(): boolean {
+        return file !== undefined;
+    }
+
+    function isYaml(): boolean {
+        return file !== undefined && file.name.endsWith("yaml");
+    }
+
+    function isIntegration(): boolean {
+        return isYaml() && file?.code !== undefined && CamelDefinitionYaml.yamlIsIntegration(file.code);
+    }
+
+    function isProperties(): boolean {
+        return file !== undefined && file.name.endsWith("properties");
+    }
+
+    function isJava(): boolean {
+        return file !== undefined && file.name.endsWith("java");
+    }
 
     function needCommit(): boolean {
         return project ? files.filter(f => f.lastUpdate > project.lastCommitTimestamp).length > 0 : false;
@@ -77,13 +78,14 @@ export const ProjectToolbar = (props: Props) => {
     }
 
     function addProperty() {
-        // if (file) {
-        //     const project = file ? ProjectModelApi.propertiesToProject(file?.code) : ProjectModel.createNew();
-        //     const props = project.properties;
-        //     props.push(ProjectProperty.createNew("", ""))
-        //     save(file.name, ProjectModelApi.propertiesToString(props));
-        //     setKey(Math.random().toString());
-        // }
+        if (file) {
+            const project = file ? ProjectModelApi.propertiesToProject(file?.code) : ProjectModel.createNew();
+            const props = project.properties;
+            props.push(ProjectProperty.createNew("", ""));
+            file.code = ProjectModelApi.propertiesToString(props);
+            ProjectService.saveFile(file);
+            setAddProperty(Math.random().toString());
+        }
     }
 
     function push () {
@@ -124,14 +126,16 @@ export const ProjectToolbar = (props: Props) => {
 
 
     function getFileToolbar() {
-        const {file, mode, editAdvancedProperties,
-            setEditAdvancedProperties, setUploadModalOpen} = props;
+        const { mode} = props;
         return <Toolbar id="toolbar-group-types">
             <ToolbarContent>
                 <Flex className="toolbar" direction={{default: "row"}} alignItems={{default: "alignItemsCenter"}}>
-                    {!isFile && <FlexItem>
-                        {getLastUpdatePanel()}
+                    {isJava() && <FlexItem>
+                        <Tooltip content="File size" position={TooltipPosition.bottom}>
+                            <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}
@@ -147,7 +151,7 @@ export const ProjectToolbar = (props: Props) => {
                             </Button>
                         </Tooltip>
                     </FlexItem>}
-                    {isYaml && <FlexItem>
+                    {isYaml() && <FlexItem>
                         <ToggleGroup>
                             <ToggleGroupItem text="Design" buttonId="design" isSelected={mode === "design"}
                                              onChange={s => props.setMode("design")}/>
@@ -156,7 +160,7 @@ export const ProjectToolbar = (props: Props) => {
                         </ToggleGroup>
                     </FlexItem>}
 
-                    {isProperties && <FlexItem>
+                    {isProperties() && <FlexItem>
                         <Checkbox
                             id="advanced"
                             label="Edit advanced"
@@ -164,19 +168,15 @@ export const ProjectToolbar = (props: Props) => {
                             onChange={checked => setEditAdvancedProperties(checked)}
                         />
                     </FlexItem>}
-                    {isProperties && <FlexItem>
+                    {isProperties() && <FlexItem>
                         <Button isSmall variant="primary" icon={<PlusIcon/>} onClick={e => addProperty()}>Add property</Button>
                     </FlexItem>}
 
-
-                    {isIntegration && <FlexItem>
+                    {isIntegration() && <FlexItem>
                         <Tooltip content="Download image" position={"bottom-end"}>
                             <Button isSmall variant="control" icon={<DownloadImageIcon/>} onClick={e => downloadImage()}/>
                         </Tooltip>
                     </FlexItem>}
-                    {/*{isYaml && currentRunner === project.name && <FlexItem>*/}
-                    {/*    <RunnerToolbar project={project} showConsole={false} reloadOnly={true} />*/}
-                    {/*</FlexItem>}*/}
                 </Flex>
             </ToolbarContent>
         </Toolbar>
@@ -247,7 +247,8 @@ export const ProjectToolbar = (props: Props) => {
          <>
             {/*{isTemplates && getTemplatesToolbar()}*/}
             {/*{!isTemplates && getProjectToolbar()}*/}
-             {!isFile && getProjectToolbar()}
+             {!isFile() && getProjectToolbar()}
+             {isFile() && getFileToolbar()}
              {getCommitModal()}
         </>
     )
diff --git a/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx b/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx
index 41b272ca..1594c7c0 100644
--- a/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx
+++ b/karavan-app/src/main/webui/src/project/RunnerToolbar.tsx
@@ -12,17 +12,22 @@ import { useProjectStore, useRunnerStore} from "../api/ProjectStore";
 import {ProjectService} from "../api/ProjectService";
 import {shallow} from "zustand/shallow";
 
-export const RunnerToolbar = () => {
 
-    const [ status] = useRunnerStore((state) => [state.status], shallow )
-    const [ project] = useProjectStore((state) => [state.project], shallow )
+interface Props {
+    reloadOnly?: boolean
+}
+
+export const RunnerToolbar = (props: Props) => {
+
+    const [status] = useRunnerStore((state) => [state.status], shallow )
+    const [project] = useProjectStore((state) => [state.project], shallow )
 
     const isRunning = status === "running";
     const isStartingPod = status === "starting";
     const isReloadingPod = status === "reloading";
     const isDeletingPod = status === "deleting";
     return (<>
-        {(isRunning || isDeletingPod) && !isReloadingPod && <FlexItem>
+        {(isRunning || isDeletingPod) && !isReloadingPod && props.reloadOnly !== true && <FlexItem>
             <Tooltip content="Stop runner" position={TooltipPosition.bottom}>
                 <Button isLoading={isDeletingPod ? true : undefined}
                         isSmall
@@ -34,7 +39,7 @@ export const RunnerToolbar = () => {
                 </Button>
             </Tooltip>
         </FlexItem>}
-        {!isRunning && !isReloadingPod && <FlexItem>
+        {!isRunning && !isReloadingPod && props.reloadOnly !== true && <FlexItem>
             <Tooltip content="Run in development mode" position={TooltipPosition.bottom}>
                 <Button isLoading={isStartingPod ? true : undefined}
                         isSmall
diff --git a/karavan-app/src/main/webui/src/project/file/FileEditor.tsx b/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
index c58008e2..fce383d6 100644
--- a/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
+++ b/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
@@ -84,32 +84,6 @@ export const FileEditor = () => {
         )
     }
 
-    function getLogView ()  {
-        return (
-            <div>
-                {file !== undefined && file.code.length !== 0 &&
-                    <CodeBlock>
-                        <CodeBlockCode id="code-content" className="log-code">{file.code}</CodeBlockCode>
-                    </CodeBlock>}
-                {(file === undefined || file.code.length === 0) &&
-                    <div>
-                        <Skeleton width="25%" screenreaderText="Loading contents"/>
-                        <br/>
-                        <Skeleton width="33%"/>
-                        <br/>
-                        <Skeleton width="50%"/>
-                        <br/>
-                        <Skeleton width="66%"/>
-                        <br/>
-                        <Skeleton width="75%"/>
-                        <br/>
-                        <Skeleton/>
-                    </div>}
-            </div>
-        )
-    }
-
-
     function isBuildIn(): boolean {
         return ['kamelets', 'templates'].includes(project.projectId);
     }
@@ -128,7 +102,6 @@ export const FileEditor = () => {
     const isYaml = file !== undefined && file.name.endsWith("yaml");
     const isIntegration = isYaml && file?.code && CamelDefinitionYaml.yamlIsIntegration(file.code);
     const isProperties = file !== undefined && file.name.endsWith("properties");
-    const isLog = file !== undefined && file.name.endsWith("log");
     const isCode = file !== undefined && (file.name.endsWith("java") || file.name.endsWith("groovy") || file.name.endsWith("json"));
     const showDesigner = isYaml && isIntegration && mode === 'design';
     const showEditor = isCode || (isYaml && !isIntegration) || (isYaml && mode === 'code');
@@ -136,7 +109,6 @@ export const FileEditor = () => {
         <>
             {showDesigner && getDesigner()}
             {showEditor && getEditor()}
-            {isLog && getLogView()}
             {isProperties && file !== undefined && <PropertiesTable/>}
         </>
     )
diff --git a/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx b/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx
index 5b58cfeb..50ada72c 100644
--- a/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx
+++ b/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx
@@ -14,47 +14,48 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, {useState} from 'react';
+import React, {useEffect, useState} from 'react';
 import {
     Button,
     Modal,
     PageSection,
-    TextInput
 } from '@patternfly/react-core';
 import '../../designer/karavan.css';
 import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
 import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
 import {ProjectModel, ProjectProperty} from "karavan-core/lib/model/ProjectModel";
 import {useFileStore} from "../../api/ProjectStore";
-import {ProjectService} from "../../api/ProjectService";
 import {ProjectModelApi} from "karavan-core/lib/api/ProjectModelApi";
+import {shallow} from "zustand/shallow"
+import {PropertyField} from "./PropertyField";
+import {ProjectService} from "../../api/ProjectService";
 
 export const PropertiesTable = () => {
 
-    const {file, operation} = useFileStore();
     const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
-    const [editAdvanced, setEditAdvanced] = useState<boolean>(false);
     const [deleteId, setDeleteId] = useState<string | undefined>(undefined);
+    const [key, setKey] = useState<string | undefined>(undefined);
     const [properties, setProperties] = useState<ProjectProperty[]>([]);
+    const [file, editAdvancedProperties, addProperty, setAddProperty] = useFileStore((state) =>
+        [state.file, state.editAdvancedProperties, state.addProperty, state.setAddProperty], shallow)
+
+    useEffect(() => {
+        console.log("PropertiesTable useEffect");
+        setProperties(getProjectModel().properties)
+    }, [addProperty]);
 
     function save (props: ProjectProperty[]) {
-        console.log("save")
         if (file) {
             file.code = ProjectModelApi.propertiesToString(props);
-            console.log("save", file)
             ProjectService.saveFile(file);
         }
     }
 
-    function getProjectModel (): ProjectModel {
+    function getProjectModel(): ProjectModel {
         return file ? ProjectModelApi.propertiesToProject(file?.code) : ProjectModel.createNew()
     }
 
-    function changeProperty(p: ProjectProperty, field: "key" | "value", val?: string) {
-        const key: string = field === 'key' && val !== undefined ? val : p.key;
-        const value: any = field === 'value' ? val : p.value;
-        const property: ProjectProperty = {id: p.id, key: key, value: value};
-        const properties = getProjectModel().properties;
+    function changeProperty(property: ProjectProperty) {
         const props = properties.map(prop => prop.id === property.id ? property : prop);
         save(props);
     }
@@ -67,11 +68,11 @@ export const PropertiesTable = () => {
 
     function confirmDelete() {
         console.log("confirmDelete")
-        const properties = getProjectModel().properties;
         const props = properties.filter(p => p.id !== deleteId);
         save(props);
         setShowDeleteConfirmation(false);
         setDeleteId(undefined);
+        setAddProperty(Math.random().toString());
     }
 
     function getDeleteConfirmation() {
@@ -90,12 +91,6 @@ export const PropertiesTable = () => {
         </Modal>)
     }
 
-    function getTextInputField(property: ProjectProperty, field: "key" | "value", readOnly: boolean) {
-        return (<TextInput isDisabled={readOnly} isRequired={true} className="text-field" type={"text"} id={field + "-" + property.key}
-                           value={field === "key" ? property.key : property.value}
-                           onChange={val => changeProperty(property, field, val)}/>)
-    }
-
     return (
         <PageSection isFilled className="kamelets-page" padding={{default: file !== undefined ? 'noPadding' : 'padding'}}>
             <PageSection padding={{default: "noPadding"}}>
@@ -111,16 +106,9 @@ export const PropertiesTable = () => {
                         </Thead>
                         <Tbody>
                             {properties.map((property, idx: number) => {
-                                const readOnly = (property.key.startsWith("camel.jbang") || property.key.startsWith("camel.karavan")) && !editAdvanced;
+                                const readOnly = (property.key.startsWith("camel.jbang") || property.key.startsWith("camel.karavan")) && !editAdvancedProperties;
                                 return (
-                                    <Tr key={property.id}>
-                                        <Td noPadding width={10} dataLabel="key">{getTextInputField(property, "key", readOnly)}</Td>
-                                        <Td noPadding width={20} dataLabel="value">{getTextInputField(property, "value", readOnly)}</Td>
-                                        <Td noPadding isActionCell dataLabel="delete" className="delete-cell">
-                                            {!readOnly && <Button variant={"plain"} icon={<DeleteIcon/>} className={"delete-button"}
-                                                                  onClick={event => startDelete(property.id)}/>}
-                                        </Td>
-                                    </Tr>
+                                    <PropertyField property={property} readOnly={readOnly} changeProperty={changeProperty} onDelete={startDelete}/>
                                 )})}
                         </Tbody>
                     </TableComposable>}
diff --git a/karavan-app/src/main/webui/src/project/file/PropertyField.tsx b/karavan-app/src/main/webui/src/project/file/PropertyField.tsx
new file mode 100644
index 00000000..a78b1c4a
--- /dev/null
+++ b/karavan-app/src/main/webui/src/project/file/PropertyField.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+import React, {useEffect, useState} from 'react';
+import {
+    Button,
+    TextInput
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import {ProjectProperty} from "karavan-core/lib/model/ProjectModel";
+import {Td, Tr} from "@patternfly/react-table";
+import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
+
+interface Props {
+    property: ProjectProperty,
+    readOnly: boolean,
+    changeProperty: (p: ProjectProperty) => void
+    onDelete: (id: string) => void
+}
+
+export const PropertyField = (props: Props) => {
+
+    const [key, setKey] = useState<string | undefined>(props.property.key);
+    const [value, setValue] = useState<string | undefined>(props.property.value);
+
+    useEffect(() => {
+        console.log("PropertyField useEffect", props.property);
+    }, []);
+
+    return (
+        <Tr key={props.property.id}>
+            <Td noPadding width={10} dataLabel="key">
+                <TextInput isDisabled={props.readOnly} isRequired={true} className="text-field" type={"text"}
+                           id={"key-" + props.property.id}
+                           value={key}
+                           onChange={(val, e) => {
+                               e.preventDefault();
+                               setKey(val)
+                               props.changeProperty?.call(this, new ProjectProperty({id: props.property.id, key: val, value: value}));
+                           }}/>
+            </Td>
+            <Td noPadding width={20} dataLabel="value">
+                <TextInput isDisabled={props.readOnly} isRequired={true} className="text-field" type={"text"}
+                           id={"value-" + props.property.id}
+                           value={value }
+                           onChange={(val, e) => {
+                               e.preventDefault();
+                               setValue(val);
+                               props.changeProperty?.call(this, new ProjectProperty({id: props.property.id, key: key, value: val}));
+                           }}/>
+            </Td>
+            <Td noPadding isActionCell dataLabel="delete" className="delete-cell">
+                {!props.readOnly && <Button variant={"plain"} icon={<DeleteIcon/>} className={"delete-button"}
+                                      onClick={event => {
+                                          props.onDelete?.call(this, props.property.id)
+                                      }}/>}
+            </Td>
+        </Tr>
+
+    )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/project/log/ProjectLog.tsx b/karavan-app/src/main/webui/src/project/log/ProjectLog.tsx
index 61d7280d..6964a897 100644
--- a/karavan-app/src/main/webui/src/project/log/ProjectLog.tsx
+++ b/karavan-app/src/main/webui/src/project/log/ProjectLog.tsx
@@ -3,6 +3,7 @@ import '../../designer/karavan.css';
 import {LogViewer} from '@patternfly/react-log-viewer';
 import {useLogStore} from "../../api/ProjectStore";
 import {shallow} from "zustand/shallow"
+import {Bullseye, Page, PageSection, PageSectionVariants, Skeleton, Spinner} from "@patternfly/react-core";
 
 interface Props {
     autoScroll: boolean
@@ -15,7 +16,10 @@ export const ProjectLog = (props: Props) => {
     const [data, currentLine] = useLogStore((state) => [state.data, state.currentLine], shallow );
     const [logViewerRef] = useState(React.createRef());
 
-    return (<LogViewer
+    return (
+        data.length > 0
+        ?
+            <LogViewer
                 isTextWrapped={props.isTextWrapped}
                 innerRef={logViewerRef}
                 hasLineNumbers={false}
@@ -24,5 +28,12 @@ export const ProjectLog = (props: Props) => {
                 height={"100vh"}
                 data={data}
                 scrollToRow={props.autoScroll ? currentLine : undefined}
-                theme={'dark'}/>);
+                theme={'dark'}/>
+        :
+            <PageSection variant={PageSectionVariants.darker}>
+                <Bullseye>
+                    <Spinner isSVG diameter="80px" aria-label="Loading..."/>
+                </Bullseye>
+            </PageSection>
+    );
 }