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/12/08 15:17:52 UTC

(camel-karavan) branch main updated: #973 - Don't copy project if existing project-id is used (#992)

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


The following commit(s) were added to refs/heads/main by this push:
     new 0be5a10e #973 - Don't copy project if existing project-id is used (#992)
0be5a10e is described below

commit 0be5a10ed26c5dc2da6e3e14385c95961856a0f7
Author: Mario Volf <mv...@users.noreply.github.com>
AuthorDate: Fri Dec 8 16:17:47 2023 +0100

    #973 - Don't copy project if existing project-id is used (#992)
    
    * mvolf - web - Implement copy project functionality
    
    * mvolf - web - Implement copy project functionality
    
    * #973 - Copy project
    
    * #973 - Copy project
    
    * #973 - Don't copy project if existing project-id is used.
    
    * #973 - Fixed code formatting
    
    * #973 - Handle duplicate project id on project create and copy. Some additional small modifications and fixes.
    
    ---------
    
    Co-authored-by: mvolf <ma...@lateral-thinking.co>
---
 karavan-designer/src/designer/utils/CamelUi.tsx    |  2 +-
 karavan-space/src/designer/utils/CamelUi.tsx       |  2 +-
 .../apache/camel/karavan/api/ProjectResource.java  | 27 ++++++++++++---
 .../camel/karavan/service/ProjectService.java      | 32 ++++++++++++------
 .../shared/exception/ProjectExistsException.java   |  7 ++++
 .../src/main/webui/src/api/KaravanApi.tsx          | 39 ++++++++++++++++------
 .../src/main/webui/src/api/ProjectService.ts       | 23 +++----------
 .../src/main/webui/src/designer/utils/CamelUi.tsx  |  2 +-
 .../main/webui/src/projects/CreateProjectModal.tsx | 39 ++++++++++++++++------
 .../main/webui/src/services/CreateServiceModal.tsx | 34 ++++++++++++++-----
 .../webui/src/shared/error/ProjectExistsError.ts   |  6 ++++
 .../webui/src/templates/CreateProjectModal.tsx     | 39 ++++++++++++++++------
 .../src/main/webui/src/util/StringUtils.ts         |  2 +-
 13 files changed, 176 insertions(+), 78 deletions(-)

diff --git a/karavan-designer/src/designer/utils/CamelUi.tsx b/karavan-designer/src/designer/utils/CamelUi.tsx
index 318c729a..c9090e18 100644
--- a/karavan-designer/src/designer/utils/CamelUi.tsx
+++ b/karavan-designer/src/designer/utils/CamelUi.tsx
@@ -303,7 +303,7 @@ export class CamelUi {
     }
 
     static nameFromTitle = (title: string): string => {
-        return title.replace(/[^a-z0-9+]+/gi, "-").toLowerCase();
+        return title.trim().replace(/[^a-z0-9+]+/gi, "-").toLowerCase();
     }
 
     static javaNameFromTitle = (title: string): string => {
diff --git a/karavan-space/src/designer/utils/CamelUi.tsx b/karavan-space/src/designer/utils/CamelUi.tsx
index 318c729a..c9090e18 100644
--- a/karavan-space/src/designer/utils/CamelUi.tsx
+++ b/karavan-space/src/designer/utils/CamelUi.tsx
@@ -303,7 +303,7 @@ export class CamelUi {
     }
 
     static nameFromTitle = (title: string): string => {
-        return title.replace(/[^a-z0-9+]+/gi, "-").toLowerCase();
+        return title.trim().replace(/[^a-z0-9+]+/gi, "-").toLowerCase();
     }
 
     static javaNameFromTitle = (title: string): string => {
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
index 52cfe36e..6acc57f5 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectResource.java
@@ -29,6 +29,7 @@ import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
 import org.apache.camel.karavan.service.ConfigService;
 import org.apache.camel.karavan.service.ProjectService;
+import org.apache.camel.karavan.shared.exception.ProjectExistsException;
 import org.jboss.logging.Logger;
 
 import java.net.URLDecoder;
@@ -71,8 +72,17 @@ public class ProjectResource {
     @POST
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    public Project save(Project project) throws Exception {
-        return projectService.save(project);
+    public Response save(Project project) throws Exception {
+        try {
+            Project createdProject = projectService.save(project);
+            return Response.ok().entity(createdProject).build();
+        } catch (ProjectExistsException exception) {
+            LOGGER.error(exception.getMessage());
+            return Response.status(Response.Status.CONFLICT).entity(exception.getMessage()).build();
+        } catch (Exception exception) {
+            LOGGER.error(exception.getMessage());
+            return Response.serverError().entity(exception.getMessage()).build();
+        }
     }
 
     @DELETE
@@ -153,7 +163,16 @@ public class ProjectResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
     @Path("/copy/{sourceProject}")
-    public Project copy(@PathParam("sourceProject") String sourceProject, Project project) throws Exception {
-        return projectService.copy(sourceProject, project);
+    public Response copy(@PathParam("sourceProject") String sourceProject, Project project) throws Exception {
+        try {
+            Project copiedProject = projectService.copy(sourceProject, project);
+            return Response.ok().entity(copiedProject).build();
+        } catch (ProjectExistsException exception) {
+            LOGGER.error(exception.getMessage());
+            return Response.status(Response.Status.CONFLICT).entity(exception.getMessage()).build();
+        } catch (Exception exception) {
+            LOGGER.error(exception.getMessage());
+            return Response.serverError().entity(exception.getMessage()).build();
+        }
     }
 }
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
index eeeb6c08..3634fee3 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
@@ -35,6 +35,7 @@ import org.apache.camel.karavan.infinispan.model.ProjectFile;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
 import org.apache.camel.karavan.registry.RegistryService;
 import org.apache.camel.karavan.shared.Property;
+import org.apache.camel.karavan.shared.exception.ProjectExistsException;
 import org.apache.commons.lang3.StringUtils;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -168,24 +169,33 @@ public class ProjectService implements HealthCheck {
     }
 
     public Project save(Project project) throws Exception {
-        boolean isNew = infinispanService.getProject(project.getProjectId()) == null;
+        boolean projectExists = infinispanService.getProject(project.getProjectId()) != null;
+        if(projectExists) {
+            throw new ProjectExistsException("Project with project id [" + project.getProjectId() + "] already exists");
+        }
+
         infinispanService.saveProject(project);
-        if (isNew) {
-            ProjectFile appProp = codeService.getApplicationProperties(project);
-            infinispanService.saveProjectFile(appProp);
-            if (!ConfigService.inKubernetes()) {
-                ProjectFile projectCompose = codeService.createInitialProjectCompose(project);
-                infinispanService.saveProjectFile(projectCompose);
-            } else if (kubernetesService.isOpenshift()){
-                ProjectFile projectCompose = codeService.createInitialDeployment(project);
-                infinispanService.saveProjectFile(projectCompose);
-            }
+
+        ProjectFile appProp = codeService.getApplicationProperties(project);
+        infinispanService.saveProjectFile(appProp);
+        if (!ConfigService.inKubernetes()) {
+            ProjectFile projectCompose = codeService.createInitialProjectCompose(project);
+            infinispanService.saveProjectFile(projectCompose);
+        } else if (kubernetesService.isOpenshift()){
+            ProjectFile projectCompose = codeService.createInitialDeployment(project);
+            infinispanService.saveProjectFile(projectCompose);
         }
+
         return project;
     }
 
     public Project copy(String sourceProjectId, Project project) throws Exception {
+        boolean projectExists = infinispanService.getProject(project.getProjectId()) != null;
+        if(projectExists) {
+            throw new ProjectExistsException("Project with project id [" + project.getProjectId() + "] already exists");
+        }
         Project sourceProject = infinispanService.getProject(sourceProjectId);
+
         // Save project
         infinispanService.saveProject(project);
 
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ProjectExistsException.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ProjectExistsException.java
new file mode 100644
index 00000000..478028bb
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/shared/exception/ProjectExistsException.java
@@ -0,0 +1,7 @@
+package org.apache.camel.karavan.shared.exception;
+
+public class ProjectExistsException extends RuntimeException {
+    public ProjectExistsException(String message) {
+        super(message);
+    }
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
index 00b3441f..5c01bd6f 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import axios, {AxiosResponse } from "axios";
+import axios, {AxiosResponse} from "axios";
 import {
     AppConfig,
     CamelStatus,
@@ -28,6 +28,7 @@ import {Buffer} from 'buffer';
 import {SsoApi} from "./SsoApi";
 import {EventStreamContentType, fetchEventSource} from "@microsoft/fetch-event-source";
 import {ProjectEventBus} from "./ProjectEventBus";
+import {ProjectExistsError} from "../shared/error/ProjectExistsError";
 
 axios.defaults.headers.common['Accept'] = 'application/json';
 axios.defaults.headers.common['Content-Type'] = 'application/json';
@@ -219,22 +220,38 @@ export class KaravanApi {
         });
     }
 
-    static async postProject(project: Project, after: (res: AxiosResponse<any>) => void) {
-        instance.post('/api/project', project)
+    static async postProject(project: Project): Promise<[Error | null, Project | null]> {
+        return instance.post('/api/project', project)
             .then(res => {
-                after(res);
+                if(res.status === 200 || res.status === 201) {
+                    return [null, res.data as Project] as [null, Project]
+                } else {
+                    return [Error("Error while creating project"), null] as [Error, null]
+                }
             }).catch(err => {
-            after(err);
-        });
+                if(err.response?.status === 409) {
+                    return [new ProjectExistsError("Project with id " + project.projectId + " already exists."), null] as [Error, null]
+                } else {
+                    return [err as Error, null] as [Error, null];
+                }
+            });
     }
 
-    static async copyProject(sourceProject: string, project: Project, after: (res: AxiosResponse<any>) => void) {
-        instance.post('/api/project/copy/' + sourceProject, project)
+    static async copyProject(sourceProject: string, project: Project): Promise<[Error | null, Project | null]> {
+        return instance.post('/api/project/copy/' + sourceProject, project)
             .then(res => {
-                after(res);
+                if(res.status === 200 || res.status === 201) {
+                    return [null, res.data as Project] as [null, Project]
+                } else {
+                    return [Error("Error while copying project"), null] as [Error, null]
+                }
             }).catch(err => {
-            after(err);
-        });
+                if(err.response?.status === 409) {
+                    return [new ProjectExistsError("Project with id " + project.projectId + " already exists."), null] as [Error, null]
+                } else {
+                    return [err as Error, null] as [Error, null];
+                }
+            });
     }
 
     static async deleteProject(project: Project, after: (res: AxiosResponse<any>) => void) {
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
index 66f9c88f..9f64e7f0 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts
@@ -236,27 +236,12 @@ export class ProjectService {
         });
     }
 
-    public static createProject(project: Project) {
-        KaravanApi.postProject(project, res => {
-            if (res.status === 200 || res.status === 201) {
-                ProjectService.refreshProjectData(project.projectId);
-                ProjectService.refreshProjects();
-                // this.props.toast?.call(this, 'Success', 'Project created', 'success');
-            } else {
-                // this.props.toast?.call(this, 'Error', res.status + ', ' + res.statusText, 'danger');
-            }
-        });
+    public static async createProject(project: Project) {
+        return KaravanApi.postProject(project);
     }
 
-    public static copyProject(sourceProject: string, project: Project) {
-        KaravanApi.copyProject(sourceProject, project, res => {
-            if (res.status === 200 || res.status === 201) {
-                EventBus.sendAlert( 'Success', 'Project copied', 'success');
-                ProjectService.refreshProjects();
-            } else {
-                EventBus.sendAlert( 'Warning', 'Error when copying project:' + res.statusText, 'warning');
-            }
-        });
+    public static async copyProject(sourceProject: string, project: Project) {
+        return KaravanApi.copyProject(sourceProject, project);
     }
 
     public static createFile(file: ProjectFile) {
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
index 318c729a..c9090e18 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
@@ -303,7 +303,7 @@ export class CamelUi {
     }
 
     static nameFromTitle = (title: string): string => {
-        return title.replace(/[^a-z0-9+]+/gi, "-").toLowerCase();
+        return title.trim().replace(/[^a-z0-9+]+/gi, "-").toLowerCase();
     }
 
     static javaNameFromTitle = (title: string): string => {
diff --git a/karavan-web/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx b/karavan-web/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx
index d35b9fdc..b92a2218 100644
--- a/karavan-web/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/projects/CreateProjectModal.tsx
@@ -19,7 +19,7 @@ import React, {useState} from 'react';
 import {
     Button, Form, FormGroup, FormHelperText, HelperText, HelperTextItem,
     Modal,
-    ModalVariant, TextInput
+    ModalVariant, TextInput, Text
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {useProjectStore} from "../api/ProjectStore";
@@ -28,6 +28,8 @@ import {Project} from "../api/ProjectModels";
 import {CamelUi} from "../designer/utils/CamelUi";
 import {shallow} from "zustand/shallow";
 import {isEmpty} from "../util/StringUtils";
+import {EventBus} from "../designer/utils/EventBus";
+import {ProjectExistsError} from "../shared/error/ProjectExistsError";
 
 export function CreateProjectModal () {
 
@@ -35,6 +37,7 @@ export function CreateProjectModal () {
     const [name, setName] = useState('');
     const [description, setDescription] = useState('');
     const [projectId, setProjectId] = useState('');
+    const [isValidationError, setIsValidationError] = useState(false);
 
     function cleanValues() {
         setName("");
@@ -47,17 +50,30 @@ export function CreateProjectModal () {
         cleanValues();
     }
 
-    function confirmAndCloseModal() {
-        operation !== 'copy' ?
-            ProjectService.createProject(new Project({name: name, description: description, projectId: projectId})) :
-            ProjectService.copyProject(project?.projectId, new Project({name: name, description: description, projectId: projectId}));
-        setOperation('none');
-        cleanValues();
+    async function handleFormSubmit() {
+        setIsValidationError(false);
+        const [ err, createdProject ] = operation !== 'copy' ?
+            await ProjectService.createProject(new Project({name: name, description: description, projectId: projectId})) :
+            await ProjectService.copyProject(project?.projectId, new Project({name: name, description: description, projectId: projectId}));
+
+        if (createdProject !== null) {
+            EventBus.sendAlert( 'Success', 'Project created', 'success');
+            ProjectService.refreshProjectData(project.projectId);
+            ProjectService.refreshProjects();
+            setOperation('none');
+            cleanValues();
+        } else if (err !== null && err instanceof ProjectExistsError) {
+            setIsValidationError(true);
+        } else {
+            operation !== 'copy' ?
+                EventBus.sendAlert( 'Warning', 'Error when creating project:' + err?.message, 'warning') :
+                EventBus.sendAlert( 'Warning', 'Error when copying project:' + err?.message, 'warning');
+        }
     }
 
     function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
         if (event.key === 'Enter' && !isEmpty(name) && !isEmpty(description) && !isEmpty(projectId)) {
-            confirmAndCloseModal();
+            handleFormSubmit();
         }
     }
 
@@ -71,7 +87,7 @@ export function CreateProjectModal () {
             onKeyDown={onKeyDown}
             actions={[
                 <Button key="confirm" variant="primary" isDisabled={!isReady}
-                        onClick={confirmAndCloseModal}>Save</Button>,
+                        onClick={handleFormSubmit}>Save</Button>,
                 <Button key="cancel" variant="secondary" onClick={closeModal}>Cancel</Button>
             ]}
             className="new-project"
@@ -91,7 +107,10 @@ export function CreateProjectModal () {
                     <TextInput className="text-field" type="text" id="projectId" name="projectId"
                                value={projectId}
                                onFocus={e => setProjectId(projectId === '' ? CamelUi.nameFromTitle(name) : projectId)}
-                               onChange={(_, e) => setProjectId(CamelUi.nameFromTitle(e))}/>
+                               onChange={(_, e) => setProjectId(CamelUi.nameFromTitle(e))}
+                               validated={isValidationError ? 'error' : 'default'}
+                    />
+                    {isValidationError && <Text  style={{ color: 'red', fontStyle: 'italic'}}>Project ID must be unique</Text>}
                     <FormHelperText>
                         <HelperText>
                             <HelperTextItem>Unique project name</HelperTextItem>
diff --git a/karavan-web/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx b/karavan-web/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx
index 80f34c5e..38977138 100644
--- a/karavan-web/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/services/CreateServiceModal.tsx
@@ -19,14 +19,15 @@ import React, {useState} from 'react';
 import {
     Button, Form, FormGroup, FormHelperText, HelperText, HelperTextItem,
     Modal,
-    ModalVariant, Radio, TextInput,
+    ModalVariant, TextInput, Text
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
-import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
 import {useAppConfigStore, useProjectStore} from "../api/ProjectStore";
 import {ProjectService} from "../api/ProjectService";
 import {Project} from "../api/ProjectModels";
 import {CamelUi} from "../designer/utils/CamelUi";
+import {EventBus} from "../designer/utils/EventBus";
+import {ProjectExistsError} from "../shared/error/ProjectExistsError";
 
 
 export function CreateServiceModal () {
@@ -37,6 +38,7 @@ export function CreateServiceModal () {
     const [runtime, setRuntime] = useState('');
     const [projectId, setProjectId] = useState('');
     const {config} = useAppConfigStore();
+    const [isValidationError, setIsValidationError] = useState(false);
 
     function cleanValues()  {
         setName("");
@@ -50,15 +52,26 @@ export function CreateServiceModal () {
         cleanValues();
     }
 
-    function confirmAndCloseModal () {
-        ProjectService.createProject(new Project({name: name, description: description, projectId: projectId}));
-        useProjectStore.setState({operation: "none"});
-        cleanValues();
+    async function handleFormSubmit () {
+        setIsValidationError(false);
+        const [ err, createdProject ] = await ProjectService.createProject(new Project({name: name, description: description, projectId: projectId}));
+
+        if (createdProject !== null) {
+            EventBus.sendAlert( 'Success', 'Project created', 'success');
+            ProjectService.refreshProjectData(project.projectId);
+            ProjectService.refreshProjects();
+            useProjectStore.setState({operation: "none"});
+            cleanValues();
+        } else if (err !== null && err instanceof ProjectExistsError) {
+            setIsValidationError(true);
+        } else {
+            EventBus.sendAlert( 'Warning', 'Error when creating project:' + err?.message, 'warning');
+        }
     }
 
     function onKeyDown (event: React.KeyboardEvent<HTMLDivElement>): void {
         if (event.key === 'Enter' && name !== undefined && description !== undefined && projectId !== undefined) {
-            confirmAndCloseModal();
+            handleFormSubmit();
         }
     }
 
@@ -72,7 +85,7 @@ export function CreateServiceModal () {
             onKeyDown={onKeyDown}
             actions={[
                 <Button key="confirm" variant="primary" isDisabled={!isReady}
-                        onClick={confirmAndCloseModal}>Save</Button>,
+                        onClick={handleFormSubmit}>Save</Button>,
                 <Button key="cancel" variant="secondary" onClick={closeModal}>Cancel</Button>
             ]}
             className="new-project"
@@ -92,7 +105,10 @@ export function CreateServiceModal () {
                     <TextInput className="text-field" type="text" id="projectId" name="projectId"
                                value={projectId}
                                onFocus={e => setProjectId(projectId === '' ? CamelUi.nameFromTitle(name) : projectId)}
-                               onChange={(_, e) => setProjectId(CamelUi.nameFromTitle(e))}/>
+                               onChange={(_, e) => setProjectId(CamelUi.nameFromTitle(e))}
+                               validated={isValidationError ? 'error' : 'default'}
+                    />
+                    {isValidationError && <Text  style={{ color: 'red', fontStyle: 'italic'}}>Project ID must be unique</Text>}
                     <FormHelperText>
                         <HelperText>
                             <HelperTextItem>Unique service name</HelperTextItem>
diff --git a/karavan-web/karavan-app/src/main/webui/src/shared/error/ProjectExistsError.ts b/karavan-web/karavan-app/src/main/webui/src/shared/error/ProjectExistsError.ts
new file mode 100644
index 00000000..6adee70b
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/shared/error/ProjectExistsError.ts
@@ -0,0 +1,6 @@
+export class ProjectExistsError extends Error {
+    constructor(message: string) {
+        super(message);
+        this.name = 'ProjectExistsError';
+    }
+}
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/templates/CreateProjectModal.tsx b/karavan-web/karavan-app/src/main/webui/src/templates/CreateProjectModal.tsx
index 09850a90..b6300be9 100644
--- a/karavan-web/karavan-app/src/main/webui/src/templates/CreateProjectModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/templates/CreateProjectModal.tsx
@@ -19,13 +19,15 @@ import React, {useState} from 'react';
 import {
     Button, Form, FormGroup, FormHelperText, HelperText, HelperTextItem,
     Modal,
-    ModalVariant, TextInput
+    ModalVariant, TextInput, Text
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import {useProjectStore} from "../api/ProjectStore";
 import {ProjectService} from "../api/ProjectService";
 import {Project} from "../api/ProjectModels";
 import {CamelUi} from "../designer/utils/CamelUi";
+import {EventBus} from "../designer/utils/EventBus";
+import {ProjectExistsError} from "../shared/error/ProjectExistsError";
 
 
 export function CreateProjectModal () {
@@ -34,6 +36,7 @@ export function CreateProjectModal () {
     const [name, setName] = useState('');
     const [description, setDescription] = useState('');
     const [projectId, setProjectId] = useState('');
+    const [isValidationError, setIsValidationError] = useState(false);
 
     function cleanValues() {
         setName("");
@@ -46,17 +49,30 @@ export function CreateProjectModal () {
         cleanValues();
     }
 
-    function confirmAndCloseModal() {
-        operation !== 'copy' ?
-            ProjectService.createProject(new Project({name: name, description: description, projectId: projectId})) :
-            ProjectService.copyProject(project?.projectId, new Project({name: name, description: description, projectId: projectId}));
-        useProjectStore.setState({operation: "none"});
-        cleanValues();
+    async function handleFormSubmit() {
+        setIsValidationError(false);
+        const [ err, createdProject ] = operation !== 'copy' ?
+            await ProjectService.createProject(new Project({name: name, description: description, projectId: projectId})) :
+            await ProjectService.copyProject(project?.projectId, new Project({name: name, description: description, projectId: projectId}));
+
+        if (createdProject !== null) {
+            EventBus.sendAlert( 'Success', 'Project created', 'success');
+            ProjectService.refreshProjectData(project.projectId);
+            ProjectService.refreshProjects();
+            useProjectStore.setState({operation: "none"});
+            cleanValues();
+        } else if (err !== null && err instanceof ProjectExistsError) {
+            setIsValidationError(true);
+        } else {
+            operation !== 'copy' ?
+                EventBus.sendAlert( 'Warning', 'Error when creating project:' + err?.message, 'warning') :
+                EventBus.sendAlert( 'Warning', 'Error when copying project:' + err?.message, 'warning');
+        }
     }
 
     function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
         if (event.key === 'Enter' && name !== undefined && description !== undefined && projectId !== undefined) {
-            confirmAndCloseModal();
+            handleFormSubmit();
         }
     }
 
@@ -70,7 +86,7 @@ export function CreateProjectModal () {
             onKeyDown={onKeyDown}
             actions={[
                 <Button key="confirm" variant="primary" isDisabled={!isReady}
-                        onClick={confirmAndCloseModal}>Save</Button>,
+                        onClick={handleFormSubmit}>Save</Button>,
                 <Button key="cancel" variant="secondary" onClick={closeModal}>Cancel</Button>
             ]}
             className="new-project"
@@ -90,7 +106,10 @@ export function CreateProjectModal () {
                     <TextInput className="text-field" type="text" id="projectId" name="projectId"
                                value={projectId}
                                onFocus={e => setProjectId(projectId === '' ? CamelUi.nameFromTitle(name) : projectId)}
-                               onChange={(_, e) => setProjectId(CamelUi.nameFromTitle(e))}/>
+                               onChange={(_, e) => setProjectId(CamelUi.nameFromTitle(e))}
+                               validated={isValidationError ? 'error' : 'default'}
+                    />
+                    {isValidationError && <Text  style={{ color: 'red', fontStyle: 'italic'}}>Project ID must be unique</Text>}
                     <FormHelperText>
                         <HelperText>
                             <HelperTextItem>Unique project name</HelperTextItem>
diff --git a/karavan-web/karavan-app/src/main/webui/src/util/StringUtils.ts b/karavan-web/karavan-app/src/main/webui/src/util/StringUtils.ts
index 5bf90a49..407778f1 100644
--- a/karavan-web/karavan-app/src/main/webui/src/util/StringUtils.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/util/StringUtils.ts
@@ -1,3 +1,3 @@
 export function isEmpty(str: string) {
-    return str === null || str === undefined || str.trim() === '';
+    return !str?.trim();
 }
\ No newline at end of file