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 86a7de952c4eadc983bdaf94d6d5571ec4f86903
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Wed Sep 13 14:54:46 2023 -0400

    UI Consistency for #885
---
 .../src/designer/editor/CodeEditor.tsx             |  0
 .../src/main/webui/src/api/ProjectStore.ts         |  6 --
 .../main/webui/src/designer/KaravanDesigner.tsx    | 42 +++++++----
 .../src/main/webui/src/designer/KaravanStore.ts    |  8 ++
 .../main/webui/src/designer/editor/CodeEditor.tsx  | 63 ++++++++++++++++
 .../src/main/webui/src/designer/karavan.css        |  9 ++-
 .../src/main/webui/src/designer/utils/EventBus.ts  |  2 +-
 .../main/webui/src/designer/utils/KaravanIcons.tsx | 15 ++++
 .../main/webui/src/designer/utils/Notification.tsx |  2 +-
 .../src/main/webui/src/project/ProjectToolbar.tsx  | 35 +--------
 .../src/main/webui/src/project/file/FileEditor.tsx | 13 ++--
 .../webui/src/project/file/PropertiesPanel.tsx     | 87 ++++++++++++++++++++++
 .../webui/src/project/file/PropertiesTable.tsx     | 60 +++++++--------
 .../webui/src/project/file/PropertiesToolbar.tsx   | 63 ++++++++++++++++
 14 files changed, 310 insertions(+), 95 deletions(-)

diff --git a/karavan-designer/src/designer/editor/CodeEditor.tsx 
b/karavan-designer/src/designer/editor/CodeEditor.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts 
b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
index 8e14aa29..1f2d6391 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts
@@ -182,8 +182,6 @@ interface FileState {
     setEditAdvancedProperties: (editAdvancedProperties: boolean) => void;
     addProperty: string;
     setAddProperty: (addProperty: string) => void;
-    mode: "design" | "code",
-    setMode: (mode: "design" | "code") => void;
 }
 
 export const useFileStore = createWithEqualityFn<FileState>((set) => ({
@@ -191,10 +189,6 @@ export const useFileStore = 
createWithEqualityFn<FileState>((set) => ({
     operation: "none",
     editAdvancedProperties: false,
     addProperty: '',
-    mode: "design",
-    setMode: (mode: "design" | "code") => {
-        set(() => ({mode: mode}));
-    },
     setFile: (operation:  "create" | "select" | "delete"| "none" | "copy" | 
"upload", file?: ProjectFile) => {
         set((state: FileState) => ({
             file: file,
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx 
b/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
index badf8ba8..cb58a377 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
@@ -17,13 +17,15 @@
 import React, {useEffect, useState} from 'react';
 import {
     Badge,
+    Button,
     PageSection,
     PageSectionVariants,
-    Switch,
     Tab,
     Tabs,
-    TabTitleIcon, TabTitleText,
+    TabTitleIcon,
+    TabTitleText,
     Tooltip,
+    TooltipPosition,
 } from '@patternfly/react-core';
 import './karavan.css';
 import {RouteDesigner} from "./route/RouteDesigner";
@@ -35,9 +37,11 @@ import {useDesignerStore, useIntegrationStore} from 
"./KaravanStore";
 import {shallow} from "zustand/shallow";
 import {getDesignerIcon} from "./utils/KaravanIcons";
 import {InfrastructureAPI} from "./utils/InfrastructureAPI";
-import {EventBus, IntegrationUpdate, ToastMessage} from "./utils/EventBus";
+import {EventBus, IntegrationUpdate} from "./utils/EventBus";
 import {RestDesigner} from "./rest/RestDesigner";
 import {BeansDesigner} from "./beans/BeansDesigner";
+import {CodeEditor} from "./editor/CodeEditor";
+import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
 
 interface Props {
     onSave: (filename: string, yaml: string, propertyOnly: boolean) => void
@@ -48,13 +52,14 @@ interface Props {
     dark: boolean
     hideLogDSL?: boolean
     tab?: string
+    showCodeTab: boolean
 }
 
-export function KaravanDesigner (props: Props) {
+export function KaravanDesigner(props: Props) {
 
     const [tab, setTab] = useState<string>('routes');
-    const [setDark, hideLogDSL, setHideLogDSL, setSelectedStep, reset] = 
useDesignerStore((s) =>
-        [s.setDark, s.hideLogDSL, s.setHideLogDSL, s.setSelectedStep, 
s.reset], shallow)
+    const [setDark, hideLogDSL, setHideLogDSL, setSelectedStep, reset, badge, 
message] = useDesignerStore((s) =>
+        [s.setDark, s.hideLogDSL, s.setHideLogDSL, s.setSelectedStep, s.reset, 
s.notificationBadge, s.notificationMessage], shallow)
     const [integration, setIntegration] = useIntegrationStore((s) =>
         [s.integration, s.setIntegration], shallow)
 
@@ -101,20 +106,23 @@ export function KaravanDesigner (props: Props) {
         return CamelDefinitionYaml.integrationToYaml(clone);
     }
 
-    function getTab(title: string, tooltip: string, icon: string) {
+    function getTab(title: string, tooltip: string, icon: string, showBadge: 
boolean = false) {
         const counts = CamelUi.getFlowCounts(integration);
         const count = counts.has(icon) && counts.get(icon) ? counts.get(icon) 
: undefined;
         const showCount = count && count > 0;
+        const color= showBadge && badge ? "red" : "initial";
         return (
-            <Tooltip position={"bottom"}
-                     content={<div>{tooltip}</div>}>
-                <div className="top-menu-item">
-                    <TabTitleIcon>{getDesignerIcon(icon)}</TabTitleIcon>
-                    <TabTitleText>{title}</TabTitleText>
-                    {showCount && <Badge isRead 
className="count">{counts.get(icon)}</Badge>}
-                </div>
-            </Tooltip>
-
+            <div className="top-menu-item" style={{color: color}}>
+                <TabTitleIcon>{getDesignerIcon(icon)}</TabTitleIcon>
+                <TabTitleText>{title}</TabTitleText>
+                {showCount && <Badge isRead 
className="count">{counts.get(icon)}</Badge>}
+                {showBadge && badge &&
+                    <Button variant="link"
+                         icon={<BellIcon color="red"/>}
+                         style={{visibility: (badge ? 'visible' : 'hidden'), 
padding: '0', margin: '0'}}
+                         onClick={event => EventBus.sendAlert(message[0], 
message[1], 'danger')}/>
+                }
+            </div>
         )
     }
 
@@ -132,6 +140,7 @@ export function KaravanDesigner (props: Props) {
                     <Tab eventKey='routes' title={getTab("Routes", 
"Integration flows", "routes")}></Tab>
                     <Tab eventKey='rest' title={getTab("REST", "REST 
services", "rest")}></Tab>
                     <Tab eventKey='beans' title={getTab("Beans", "Beans 
Configuration", "beans")}></Tab>
+                    {props.showCodeTab && <Tab eventKey='code' 
title={getTab("YAML", "YAML Code", "code", true)}></Tab>}
                 </Tabs>
                 {/*{tab === 'routes' && <Tooltip content={"Hide Log 
elements"}>*/}
                 {/*    <Switch*/}
@@ -150,6 +159,7 @@ export function KaravanDesigner (props: Props) {
             {tab === 'routes' && <RouteDesigner/>}
             {tab === 'rest' && <RestDesigner/>}
             {tab === 'beans' && <BeansDesigner/>}
+            {tab === 'code' && <CodeEditor/>}
         </PageSection>
     )
 }
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/KaravanStore.ts 
b/karavan-web/karavan-app/src/main/webui/src/designer/KaravanStore.ts
index e524e625..15a9b7be 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/KaravanStore.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/KaravanStore.ts
@@ -152,6 +152,8 @@ export const useConnectionsStore = 
createWithEqualityFn<ConnectionsState>((set)
 
 type DesignerState = {
     dark: boolean;
+    notificationBadge: boolean;
+    notificationMessage: [string, string];
     hideLogDSL: boolean;
     shiftKeyPressed: boolean;
     showDeleteConfirmation: boolean;
@@ -166,6 +168,8 @@ type DesignerState = {
     left: number,
 }
 const designerState: DesignerState = {
+    notificationBadge: false,
+    notificationMessage: ['', ''],
     dark: false,
     hideLogDSL: false,
     shiftKeyPressed: false,
@@ -192,6 +196,7 @@ type DesignerAction = {
     setClipboardSteps: (clipboardSteps: CamelElement[]) => void;
     setPosition: (width: number, height: number, top: number, left: number) => 
void;
     reset: () => void;
+    setNotification: (notificationBadge: boolean, notificationMessage: 
[string, string]) => void;
 }
 
 export const useDesignerStore = createWithEqualityFn<DesignerState & 
DesignerAction>((set) => ({
@@ -240,5 +245,8 @@ export const useDesignerStore = 
createWithEqualityFn<DesignerState & DesignerAct
     },
     reset: () => {
         set(designerState);
+    },
+    setNotification: (notificationBadge: boolean, notificationMessage: 
[string, string]) => {
+        set({notificationBadge: notificationBadge, notificationMessage: 
notificationMessage})
     }
 }), shallow)
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/editor/CodeEditor.tsx 
b/karavan-web/karavan-app/src/main/webui/src/designer/editor/CodeEditor.tsx
new file mode 100644
index 00000000..323bf0cf
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/editor/CodeEditor.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 '../../designer/karavan.css';
+import Editor from "@monaco-editor/react";
+import {shallow} from "zustand/shallow";
+import {useDesignerStore, useIntegrationStore} from "../KaravanStore";
+import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml";
+import {EventBus} from "../utils/EventBus";
+
+export function CodeEditor () {
+
+    const [integration, setIntegration] = useIntegrationStore((s) => 
[s.integration, s.setIntegration], shallow);
+    const [setNotification, badge] = useDesignerStore((s) => 
[s.setNotification, s.notificationBadge], shallow)
+    const [code, setCode] = useState<string>('');
+
+    useEffect(() => {
+        const c = CamelDefinitionYaml.integrationToYaml(integration);
+        setCode(c);
+        return () => {
+            setNotification(false, ['', '']);
+        }
+    }, []);
+
+    function onChange(value: string | undefined) {
+        if (value) {
+            try {
+                const i = 
CamelDefinitionYaml.yamlToIntegration(integration.metadata.name, value);
+                setIntegration(i, false);
+                setNotification(false, ['', '']);
+            } catch (e: any) {
+                const message: string = e?.message ? e.message : e.reason;
+                setNotification(true, ['Error in YAML, Integration can not be 
saved!' ,message]);
+            }
+        }
+    }
+
+    return (
+        <Editor
+            height="100vh"
+            defaultLanguage={'yaml'}
+            theme={'light'}
+            value={code}
+            className={'code-editor'}
+            defaultValue={code}
+            onChange={(value, ev) => onChange(value)}
+        />
+    )
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/karavan.css 
b/karavan-web/karavan-app/src/main/webui/src/designer/karavan.css
index bf4d9d49..f1fd43b4 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/karavan.css
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/karavan.css
@@ -1046,8 +1046,9 @@
 
 /*REST Page*/
 .karavan .rest-page {
-    flex: 1;
-    overflow: auto;
+    /*flex: 1;*/
+    /*overflow: auto;*/
+    /*height: 100%;*/
 }
 
 .karavan .rest-page .rest-page-columns {
@@ -1066,7 +1067,7 @@
 }
 
 .karavan .rest-page .flows {
-    width: 800px;
+    /*width: 800px;*/
     margin: 0 auto 80px auto;
 }
 
@@ -1167,7 +1168,7 @@
 .karavan .rest-page .rest-config-card .description,
 .karavan .rest-page .method-card .description {
     margin: auto 0 auto 0;
-    width: 670px;
+    min-width: 200px;
     white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/utils/EventBus.ts 
b/karavan-web/karavan-app/src/main/webui/src/designer/utils/EventBus.ts
index 8f3f9712..0a20d3c6 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/utils/EventBus.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/utils/EventBus.ts
@@ -76,7 +76,7 @@ export class ToastMessage {
     id: string = ''
     text: string = ''
     title: string = ''
-    variant?: 'success' | 'danger' | 'warning' | 'info' | 'custom';
+    variant: 'success' | 'danger' | 'warning' | 'info' | 'custom';
 
     constructor(title: string, text: string, variant: 'success' | 'danger' | 
'warning' | 'info' | 'custom') {
         this.id = uuidv4();
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/utils/KaravanIcons.tsx 
b/karavan-web/karavan-app/src/main/webui/src/designer/utils/KaravanIcons.tsx
index 17560ff4..9e514046 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/utils/KaravanIcons.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/utils/KaravanIcons.tsx
@@ -263,6 +263,21 @@ export function CamelIcon(props?: (JSX.IntrinsicAttributes 
& React.SVGProps<SVGS
 }
 
 export function getDesignerIcon(icon: string) {
+    if (icon === 'code') return (
+    <svg
+        className="top-icon" id="icon"
+        xmlns="http://www.w3.org/2000/svg";
+        width="24"
+        height="24"
+        fill="none"
+        viewBox="0 0 24 24"
+    >
+        <path
+            fill="#000000"
+            d="M8.502 5.387a.75.75 0 00-1.004-1.115L5.761 
5.836c-.737.663-1.347 1.212-1.767 1.71-.44.525-.754 1.088-.754 1.784 0 .695.313 
1.258.754 1.782.42.499 1.03 1.049 1.767 1.711l1.737 1.564a.75.75 0 
101.004-1.115l-1.697-1.527c-.788-.709-1.319-1.19-1.663-1.598-.33-.393-.402-.622-.402-.817
 0-.196.072-.425.402-.818.344-.409.875-.889 1.663-1.598l1.697-1.527zM14.18 
4.275a.75.75 0 01.532.918l-3.987 15a.75.75 0 11-1.45-.386l3.987-15a.75.75 0 
01.918-.532zM15.443 10.498a.75.75 0 011.059-.05 [...]
+        ></path>
+    </svg>
+        )
     if (icon === 'routes') return (
         <svg className="top-icon" width="32px" height="32px" viewBox="0 0 32 
32" id="icon">
             <defs>
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/designer/utils/Notification.tsx 
b/karavan-web/karavan-app/src/main/webui/src/designer/utils/Notification.tsx
index f3572ff4..520062c8 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/utils/Notification.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/utils/Notification.tsx
@@ -28,7 +28,7 @@ export function Notification () {
         <AlertGroup isToast isLiveRegion>
             {alerts.map((e: ToastMessage) => (
                 <Alert key={e.id} className="main-alert" variant={e.variant} 
title={e.title}
-                       timeout={e.variant === "success" ? 1000 : 2000}
+                       timeout={['success', 'info', 
'custom'].includes(e.variant) ? 1000 : 20000}
                        actionClose={<AlertActionCloseButton onClose={() => {
                            setAlerts(prevState => {
                                return [...prevState.filter(t => t.id !== 
e.id)];
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 84f0cd2c..7a14f166 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
@@ -27,9 +27,9 @@ import {BuildToolbar} from "./BuildToolbar";
 export function ProjectToolbar () {
 
     const [project, isPushing, tabIndex] = useProjectStore((s) => [s.project, 
s.isPushing, s.tabIndex], shallow )
-    const [file, editAdvancedProperties, setEditAdvancedProperties, 
setAddProperty, mode, setMode]
+    const [file, editAdvancedProperties, setEditAdvancedProperties, 
setAddProperty]
         = useFileStore((state) =>
-        [state.file, state.editAdvancedProperties, 
state.setEditAdvancedProperties, state.setAddProperty, state.mode, 
state.setMode], shallow )
+        [state.file, state.editAdvancedProperties, 
state.setEditAdvancedProperties, state.setAddProperty], shallow )
 
     useEffect(() => {
     }, [project, file]);
@@ -58,42 +58,13 @@ export function ProjectToolbar () {
         EventBus.sendCommand("downloadImage");
     }
 
-    function addProperty() {
-        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, true);
-            setAddProperty(Math.random().toString());
-        }
-    }
+
 
     function getFileToolbar() {
         return <Toolbar id="toolbar-group-types">
             <ToolbarContent>
                 <Flex className="toolbar" direction={{default: "row"}} 
alignItems={{default: "alignItemsCenter"}}>
                     {isRunnable() && <DevModeToolbar reloadOnly={true}/>}
-                    {isIntegration() && <FlexItem>
-                        <ToggleGroup>
-                            <ToggleGroupItem text="Design" buttonId="design" 
isSelected={mode === "design"}
-                                             onChange={(_event, s) => 
setMode("design")}/>
-                            <ToggleGroupItem text="Code" buttonId="code" 
isSelected={mode === "code"}
-                                             onChange={(_event, s) => 
setMode("code")}/>
-                        </ToggleGroup>
-                    </FlexItem>}
-
-                    {isProperties() && <FlexItem>
-                        <Checkbox
-                            id="advanced"
-                            label="Edit advanced"
-                            isChecked={editAdvancedProperties}
-                             onChange={(_, checked) => 
setEditAdvancedProperties(checked)}
-                        />
-                    </FlexItem>}
-                    {isProperties() && <FlexItem>
-                        <Button size="sm" variant="primary" icon={<PlusIcon/>} 
onClick={e => addProperty()}>Add property</Button>
-                    </FlexItem>}
 
                     {isIntegration() && <FlexItem>
                         <Tooltip content="Download image" 
position={"bottom-end"}>
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/file/FileEditor.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
index 1fc3b67c..9745f2bf 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
@@ -24,6 +24,9 @@ import {KaravanDesigner} from 
"../../designer/KaravanDesigner";
 import {ProjectService} from "../../api/ProjectService";
 import {PropertiesTable} from "./PropertiesTable";
 import {shallow} from "zustand/shallow";
+import {PropertiesToolbar} from "./PropertiesToolbar";
+import {Card, Panel} from "@patternfly/react-core";
+import {PropertiesPanel} from "./PropertiesPanel";
 
 interface Props {
     projectId: string
@@ -31,8 +34,7 @@ interface Props {
 
 export function FileEditor (props: Props) {
 
-    const [file, operation, mode] = useFileStore((state) =>
-        [state.file, state.operation, state.mode, state.setMode], shallow )
+    const [file, operation] = useFileStore((state) => [state.file, 
state.operation], shallow )
 
     function save (name: string, code: string) {
         if (file) {
@@ -50,6 +52,7 @@ export function FileEditor (props: Props) {
         return (
             file !== undefined &&
             <KaravanDesigner
+                showCodeTab={true}
                 dark={false}
                 filename={file.name}
                 yaml={file.code}
@@ -86,13 +89,13 @@ export function FileEditor (props: Props) {
     const isProperties = file !== undefined && 
file.name.endsWith("properties");
     const isScript = file !== undefined && file.name.endsWith("sh");
     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') || isScript;
+    const showDesigner = isYaml && isIntegration;
+    const showEditor = isCode || (isYaml && !isIntegration) || (isYaml) || 
isScript;
     return (
         <>
             {showDesigner && getDesigner()}
             {showEditor && getEditor()}
-            {isProperties && file !== undefined && <PropertiesTable/>}
+            {isProperties && file !== undefined && <PropertiesPanel/>}
         </>
     )
 }
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesPanel.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesPanel.tsx
new file mode 100644
index 00000000..2cd41770
--- /dev/null
+++ 
b/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesPanel.tsx
@@ -0,0 +1,87 @@
+import React, {useEffect} from 'react';
+import {
+    Badge,
+    Button,
+    Bullseye,
+    EmptyState,
+    EmptyStateVariant,
+    EmptyStateIcon,
+    PageSection, PanelHeader, Panel, Tooltip, Label, EmptyStateHeader, 
PanelMain, PanelMainBody, Flex, FlexItem,
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import {
+       Tbody,
+       Td,
+       Th,
+       Thead,
+       Tr
+} from '@patternfly/react-table';
+import {
+       Table
+} from '@patternfly/react-table/deprecated';
+import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
+import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
+import {useFilesStore, useFileStore, useProjectStore} from 
"../../api/ProjectStore";
+import {getProjectFileType, ProjectFile, ProjectFileTypes} from 
"../../api/ProjectModels";
+import DownloadIcon from 
"@patternfly/react-icons/dist/esm/icons/download-icon";
+import FileSaver from "file-saver";
+import {shallow} from "zustand/shallow";
+import {PropertiesToolbar} from "./PropertiesToolbar";
+import {PropertiesTable} from "./PropertiesTable";
+
+export function PropertiesPanel () {
+
+    const [files] = useFilesStore((s) => [s.files], shallow);
+    const [project] = useProjectStore((s) => [s.project], shallow);
+    const [operation] = useFileStore((s) => [s.operation], shallow);
+
+    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 needCommit(lastUpdate: number): boolean {
+        return lastUpdate > project.lastCommitTimestamp;
+    }
+
+    function download (file: ProjectFile) {
+        if (file) {
+            const type = file.name.endsWith("yaml") ? 
"application/yaml;charset=utf-8" : undefined;
+            const f = new File([file.code], file.name, {type: type});
+            FileSaver.saveAs(f);
+        }
+    }
+
+    function isBuildIn(): boolean {
+        return ['kamelets', 'templates', 
'services'].includes(project.projectId);
+    }
+
+    function canDeleteFiles(): boolean {
+        return !['templates', 'services'].includes(project.projectId);
+    }
+
+    function isKameletsProject(): boolean {
+        return project.projectId === 'kamelets';
+    }
+
+    const types = isBuildIn()
+        ? (isKameletsProject() ? ['KAMELET'] : ['CODE', 'PROPERTIES'])
+        : ProjectFileTypes.filter(p => !['PROPERTIES', 'LOG', 
'KAMELET'].includes(p.name)).map(p => p.name);
+
+    return (
+        <PageSection padding={{default: 'noPadding'}} 
className="scrollable-out">
+            <PageSection isFilled padding={{default: 'padding'}} 
className="scrollable-in">
+            <Panel>
+                <PanelHeader>
+                    <PropertiesToolbar/>
+                </PanelHeader>
+            </Panel>
+                <PropertiesTable/>
+        </PageSection>
+        </PageSection>
+    )
+}
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx
index 1e0fe7fb..b443f32c 100644
--- 
a/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx
+++ 
b/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx
@@ -22,13 +22,13 @@ import {
 } from '@patternfly/react-core';
 import '../../designer/karavan.css';
 import {
-       Tbody,
-       Th,
-       Thead,
-       Tr
+    Tbody,
+    Th,
+    Thead,
+    Tr
 } from '@patternfly/react-table';
 import {
-       Table
+    Table
 } from '@patternfly/react-table/deprecated';
 
 import {ProjectModel, ProjectProperty} from 
"karavan-core/lib/model/ProjectModel";
@@ -38,7 +38,7 @@ import {shallow} from "zustand/shallow"
 import {PropertyField} from "./PropertyField";
 import {ProjectService} from "../../api/ProjectService";
 
-export function PropertiesTable () {
+export function PropertiesTable() {
 
     const [showDeleteConfirmation, setShowDeleteConfirmation] = 
useState<boolean>(false);
     const [deleteId, setDeleteId] = useState<string | undefined>(undefined);
@@ -52,7 +52,7 @@ export function PropertiesTable () {
         setProperties(getProjectModel().properties)
     }, [addProperty]);
 
-    function save (props: ProjectProperty[]) {
+    function save(props: ProjectProperty[]) {
         if (file) {
             file.code = ProjectModelApi.propertiesToString(props);
             ProjectService.saveFile(file, true);
@@ -100,28 +100,28 @@ export function PropertiesTable () {
     }
 
     return (
-        <PageSection isFilled className="scrollable-in" padding={{default: 
file !== undefined ? 'noPadding' : 'padding'}}>
-            <PageSection padding={{default: "noPadding"}}>
-                {properties.length > 0 &&
-                    <Table aria-label="Property table" variant='compact' 
borders={false}
-                                     className="project-properties">
-                        <Thead>
-                            <Tr>
-                                <Th key='name'>Name</Th>
-                                <Th key='value'>Value</Th>
-                                <Th></Th>
-                            </Tr>
-                        </Thead>
-                        <Tbody>
-                            {properties.map((property, idx: number) => {
-                                const readOnly = 
(property.key.startsWith("camel.jbang") || 
property.key.startsWith("camel.karavan")) && !editAdvancedProperties;
-                                return (
-                                    <PropertyField property={property} 
readOnly={readOnly} changeProperty={changeProperty} onDelete={startDelete}/>
-                                )})}
-                        </Tbody>
-                    </Table>}
-                {showDeleteConfirmation && getDeleteConfirmation()}
-            </PageSection>
-        </PageSection>
+        <>
+            {properties.length > 0 &&
+                <Table aria-label="Property table" variant='compact' 
borders={false}
+                       className="project-properties">
+                    <Thead>
+                        <Tr>
+                            <Th key='name'>Name</Th>
+                            <Th key='value'>Value</Th>
+                            <Th></Th>
+                        </Tr>
+                    </Thead>
+                    <Tbody>
+                        {properties.map((property, idx: number) => {
+                            const readOnly = 
(property.key.startsWith("camel.jbang") || 
property.key.startsWith("camel.karavan")) && !editAdvancedProperties;
+                            return (
+                                <PropertyField property={property} 
readOnly={readOnly} changeProperty={changeProperty}
+                                               onDelete={startDelete}/>
+                            )
+                        })}
+                    </Tbody>
+                </Table>}
+            {showDeleteConfirmation && getDeleteConfirmation()}
+        </>
     )
 }
\ No newline at end of file
diff --git 
a/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesToolbar.tsx 
b/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesToolbar.tsx
new file mode 100644
index 00000000..598dc95e
--- /dev/null
+++ 
b/karavan-web/karavan-app/src/main/webui/src/project/file/PropertiesToolbar.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 from 'react';
+import {
+    Button, Checkbox,
+    Flex,
+    FlexItem
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon";
+import {useFileStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {ProjectService} from "../../api/ProjectService";
+import {ProjectModelApi} from "karavan-core/lib/api/ProjectModelApi";
+import {ProjectModel, ProjectProperty} from 
"karavan-core/lib/model/ProjectModel";
+
+export function PropertiesToolbar () {
+
+    const [file, editAdvancedProperties, setEditAdvancedProperties, 
setAddProperty] = useFileStore((state) =>
+        [state.file, state.editAdvancedProperties, 
state.setEditAdvancedProperties, state.setAddProperty], shallow )
+
+
+    function addProperty() {
+        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, true);
+            setAddProperty(Math.random().toString());
+        }
+    }
+
+    return (
+        <Flex className="toolbar" direction={{default: "row"}} 
justifyContent={{default: "justifyContentFlexEnd"}}>
+            <FlexItem>
+                <Checkbox
+                    id="advanced"
+                    label="Edit advanced"
+                    isChecked={editAdvancedProperties}
+                    onChange={(_, checked) => 
setEditAdvancedProperties(checked)}
+                />
+            </FlexItem>
+            <FlexItem>
+                <Button size="sm" variant="primary" icon={<PlusIcon/>} 
onClick={e => addProperty()}>Add property</Button>
+            </FlexItem>
+        </Flex>
+    )
+}

Reply via email to