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

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

commit cd71a84bb2597e7f97ba03cd166923d4259cd6eb
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Mon Aug 24 15:38:37 2026 -0400

    Karavan Webapp UI Stores
---
 .../src/main/webui/src/stores/AccessStore.ts       | 125 +++++
 .../src/main/webui/src/stores/ActivityStore.ts     |  53 ++
 .../src/main/webui/src/stores/ArchitectureStore.ts | 143 ++++++
 .../src/main/webui/src/stores/CommitsStore.ts      |  86 ++++
 .../src/main/webui/src/stores/ComplexityStore.ts   |  41 ++
 .../webui/src/stores/ContainerStatusesStore.ts     |  63 +++
 .../webui/src/stores/DeploymentStatusesStore.ts    |  37 ++
 .../main/webui/src/stores/DocumentationStore.ts    |  53 ++
 karavan-app/src/main/webui/src/stores/LogStore.ts  |  58 +++
 .../src/main/webui/src/stores/ProjectStore.ts      | 552 +++++++++++++++++++++
 .../src/main/webui/src/stores/ReadinessStore.ts    |  40 ++
 .../src/main/webui/src/stores/SearchStore.ts       |  21 +
 .../src/main/webui/src/stores/SettingsStore.ts     |  72 +++
 .../src/main/webui/src/stores/SystemStore.ts       |  51 ++
 .../main/webui/src/stores/useKubernetesStore.ts    |  28 ++
 .../src/main/webui/src/stores/useUIStore.ts        |  46 ++
 16 files changed, 1469 insertions(+)

diff --git a/karavan-app/src/main/webui/src/stores/AccessStore.ts 
b/karavan-app/src/main/webui/src/stores/AccessStore.ts
new file mode 100644
index 00000000..2527e30b
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/AccessStore.ts
@@ -0,0 +1,125 @@
+/*
+ * 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 {createWithEqualityFn} from "zustand/traditional";
+import {shallow} from "zustand/shallow";
+import isEqual from "lodash/isEqual";
+import {AccessRole, AccessToken, AccessUser, PLATFORM_ADMIN, 
PLATFORM_DEVELOPER, SessionInfo} from "@models/AccessModels";
+import {AccessApi} from "@api/AccessApi";
+
+interface AccessState {
+    users: AccessUser[];
+    roles: AccessRole[];
+    sessions: SessionInfo[];
+    tokens: AccessToken[];
+    filter: string;
+    showUserModal: boolean;
+    showRoleModal: boolean;
+    showPasswordModal: boolean;
+    showTokenModal: boolean;
+    currentUser?: AccessUser;
+    currentToken?: AccessToken;
+
+    // Setters
+    setUsers: (users: AccessUser[]) => void;
+    setTokens: (tokens: AccessToken[]) => void;
+    setFilter: (filter: string) => void;
+    setShowUserModal: (showUserModal: boolean) => void;
+    setShowRoleModal: (showRoleModal: boolean) => void;
+    setShowPasswordModal: (showPasswordModal: boolean) => void;
+    setShowTokenModal: (showTokenModal: boolean) => void;
+    setCurrentUser: (currentUser?: AccessUser) => void;
+    setCurrentToken: (currentToken?: AccessToken) => void;
+
+    // Fetch Actions
+    fetchUsers: () => Promise<AccessUser[]>;
+    fetchRoles: () => Promise<AccessRole[]>;
+    fetchSessions: () => Promise<SessionInfo[]>;
+    fetchTokens: () => Promise<AccessToken[]>;
+    refreshAccess: () => Promise<void>;
+}
+
+export const useAccessStore = createWithEqualityFn<AccessState>((set, get) => 
({
+    // Initial State
+    users: [],
+    sessions: [],
+    tokens: [],
+    roles: [
+        new AccessRole({ name: PLATFORM_ADMIN, description: 'Administrator' }),
+        new AccessRole({ name: PLATFORM_DEVELOPER, description: 'Developer' })
+    ],
+    filter: '',
+    showUserModal: false,
+    showRoleModal: false,
+    showPasswordModal: false,
+    showTokenModal: false,
+    currentUser: undefined,
+    currentToken: undefined,
+
+    // Basic Setters
+    setUsers: (users: AccessUser[]) => set({ users }),
+    setTokens: (tokens: AccessToken[]) => set({ tokens }),
+    setFilter: (filter: string) => set({ filter: filter?.toLowerCase() }),
+    setShowUserModal: (showUserModal: boolean) => set({ showUserModal }),
+    setShowRoleModal: (showRoleModal: boolean) => set({ showRoleModal }),
+    setShowPasswordModal: (showPasswordModal: boolean) => set({ 
showPasswordModal }),
+    setShowTokenModal: (showTokenModal: boolean) => set({ showTokenModal }),
+    setCurrentUser: (currentUser?: AccessUser) => set({ currentUser }),
+    setCurrentToken: (currentToken?: AccessToken) => set({ currentToken }),
+
+    // Fetch Actions
+    fetchUsers: async (): Promise<AccessUser[]> => {
+        const users = await AccessApi.getUsers();
+        if (!isEqual(get().users, users)) {
+            set({ users });
+        }
+        return users;
+    },
+
+    fetchRoles: async (): Promise<AccessRole[]> => {
+        const roles = await AccessApi.getRoles();
+        if (!isEqual(get().roles, roles)) {
+            set({ roles });
+        }
+        return roles;
+    },
+
+    fetchSessions: async (): Promise<SessionInfo[]> => {
+        const sessions = await AccessApi.getSessions();
+        if (!isEqual(get().sessions, sessions)) {
+            set({ sessions });
+        }
+        return sessions;
+    },
+
+    fetchTokens: async (): Promise<AccessToken[]> => {
+        const tokens = await AccessApi.getTokens();
+        if (!isEqual(get().tokens, tokens)) {
+            set({ tokens });
+        }
+        return tokens;
+    },
+
+    refreshAccess: async (): Promise<void> => {
+        // Fetch all in parallel for better performance
+        await Promise.all([
+            get().fetchUsers(),
+            get().fetchRoles(),
+            get().fetchSessions(),
+            get().fetchTokens()
+        ]);
+    }
+}), shallow);
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/stores/ActivityStore.ts 
b/karavan-app/src/main/webui/src/stores/ActivityStore.ts
new file mode 100644
index 00000000..5e1e6965
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/ActivityStore.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 {KaravanApi} from "@api/KaravanApi";
+import {create} from "zustand";
+import isEqual from "lodash/isEqual";
+
+interface ActivityState {
+    projectsActivities?: any;
+    fetchProjectsActivities: () => Promise<void>;
+    usersActivities?: any;
+    fetchUsersActivities: () => Promise<void>;
+}
+
+export const useActivityStore = create<ActivityState>((set, get) => ({
+    projectsActivities: {},
+    usersActivities: {},
+    fetchProjectsActivities: async (): Promise<void> => {
+        const currentActivities = get().projectsActivities;
+        await new Promise<any>((resolve) => {
+            KaravanApi.getProjectsActivities(resolve);
+        }).then(activities=> {
+            if (!isEqual(currentActivities, activities)) {
+                set({projectsActivities: activities});
+            }
+        })
+    },
+    fetchUsersActivities: async (): Promise<void> => {
+        const currentActivities = get().usersActivities;
+        await new Promise<any>((resolve) => {
+            KaravanApi.getUsersActivities(resolve);
+        }).then(activities=> {
+            if (!isEqual(currentActivities, activities)) {
+                set({usersActivities: activities});
+            }
+        })
+    },
+}))
+
+
diff --git a/karavan-app/src/main/webui/src/stores/ArchitectureStore.ts 
b/karavan-app/src/main/webui/src/stores/ArchitectureStore.ts
new file mode 100644
index 00000000..a1ab6ed8
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/ArchitectureStore.ts
@@ -0,0 +1,143 @@
+/*
+ * 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 {createWithEqualityFn} from "zustand/traditional";
+import {ModalConfirmationProps} from "@shared/ui/ModalConfirmation";
+import {BeanUsageData, ExchangeData, ExchangeMessage} from 
"@core/model/ExchangeDefinitions";
+
+export type ArchitectureLayout = 'dagre' | 'elk' | 'force';
+
+const STORAGE_KEY_ARCHITECTURE_LAYOUT = "architecture-layout";
+const STORAGE_KEY_ARCHITECTURE_SHOW_GROUPS = "architecture-show-groups";
+const STORAGE_KEY_ARCHITECTURE_SHOW_TEMPLATES = "architecture-show-templates";
+
+function getInitialLayout(): ArchitectureLayout {
+    if (typeof window === "undefined") return "elk"; // SSR safety
+    const saved = localStorage.getItem(STORAGE_KEY_ARCHITECTURE_LAYOUT) as 
ArchitectureLayout | null;
+    return saved ?? "elk";
+}
+
+function getInitialShowGroups(): boolean {
+    if (typeof window === "undefined") return false; // SSR safety
+    const saved = localStorage.getItem(STORAGE_KEY_ARCHITECTURE_SHOW_GROUPS);
+    if (saved === null) return false;
+    return saved === "true";
+}
+function getInitialShowTemplates(): boolean {
+    if (typeof window === "undefined") return false; // SSR safety
+    const saved = 
localStorage.getItem(STORAGE_KEY_ARCHITECTURE_SHOW_TEMPLATES);
+    if (saved === null) return false;
+    return saved === "true";
+}
+
+interface ArchitectureState {
+    fileName?: string
+    setFileName: (fileName?: string) => void
+    showGroups: boolean
+    setShowGroups: (showGroups: boolean) => void
+    showLegend: boolean
+    setShowLegend: (showLegend: boolean) => void
+    showStats: boolean
+    setShowStats: (showStats: boolean) => void
+    layout: ArchitectureLayout
+    setLayout: (layout: ArchitectureLayout) => void
+    nextLayout: () => void
+    showRouteTemplates?: boolean
+    setShowRouteTemplates: (showRouteTemplates: boolean) => void
+    confirmationProps?: ModalConfirmationProps
+    setConfirmationProps: (confirmationProps?: ModalConfirmationProps) => void
+    selectedNodes: string[]
+    setSelectedNodes: (selectedNodes: string[]) => void
+    connectedToSelectedNodes: string[]
+    setConnectedSelectedNodes: (connectedToSelectedNodes: string[]) => void
+    exchangeMessage?: ExchangeMessage
+    setExchangeMessage: (exchangeMessage: ExchangeMessage) => void
+    selectedVariable?: ExchangeData
+    setSelectedVariable: (selectedVariable: ExchangeData) => void
+    selectedBean?: BeanUsageData
+    setSelectedBean: (selectedBean: BeanUsageData) => void
+}
+
+export const useArchitectureStore = 
createWithEqualityFn<ArchitectureState>((set, get) => {
+    return {
+        setFileName: (fileName?: string) => {
+            set((state: ArchitectureState) => {
+                return {fileName: fileName};
+            });
+        },
+        showGroups: getInitialShowGroups(),
+        setShowGroups: (showGroups: boolean) => {
+            localStorage.setItem(STORAGE_KEY_ARCHITECTURE_SHOW_GROUPS, 
String(showGroups));
+            set({showGroups: showGroups});
+        },
+        showLegend: false,
+        setShowLegend: (showLegend: boolean) => {
+            set((state: ArchitectureState) => {
+                return {showLegend: showLegend};
+            });
+        },
+        showStats: false,
+        setShowStats: (showStats: boolean) => {
+            set((state: ArchitectureState) => {
+                return {showStats: showStats};
+            });
+        },
+        layout: getInitialLayout(),
+        setLayout: (layout: ArchitectureLayout) => {
+            localStorage.setItem(STORAGE_KEY_ARCHITECTURE_LAYOUT, layout);
+            set({ layout });
+        },
+        nextLayout: () => {
+            const currentLayout = get().layout;
+            let nextLayout: ArchitectureLayout = 'dagre';
+            if (currentLayout === 'dagre') {
+                nextLayout = 'elk';
+            } else if (currentLayout === 'elk') {
+                nextLayout = 'force';
+            } else {
+                nextLayout = 'dagre';
+            }
+            localStorage.setItem(STORAGE_KEY_ARCHITECTURE_LAYOUT, nextLayout);
+            set({ layout: nextLayout });
+        },
+        showRouteTemplates: getInitialShowTemplates(),
+        setShowRouteTemplates: (showRouteTemplates: boolean) => {
+            localStorage.setItem(STORAGE_KEY_ARCHITECTURE_SHOW_TEMPLATES, 
String(showRouteTemplates));
+            set({ showRouteTemplates: showRouteTemplates });
+        },
+        setConfirmationProps: (confirmationProps?: ModalConfirmationProps) => {
+            set({ confirmationProps });
+        },
+        selectedNodes: [],
+        setSelectedNodes: (selectedNodes: string[]) => {
+            set({ selectedNodes });
+        },
+        connectedToSelectedNodes: [],
+        setConnectedSelectedNodes: (connectedToSelectedNodes: string[]) => {
+            set({ connectedToSelectedNodes });
+        },
+        setExchangeMessage: (exchangeMessage: ExchangeMessage) => {
+            set({ exchangeMessage });
+        },
+        setSelectedVariable: (selectedVariable: ExchangeData) => {
+            set({ selectedVariable });
+        },
+        setSelectedBean: (selectedBean: BeanUsageData) => {
+            set({ selectedBean });
+        }
+    }
+});
diff --git a/karavan-app/src/main/webui/src/stores/CommitsStore.ts 
b/karavan-app/src/main/webui/src/stores/CommitsStore.ts
new file mode 100644
index 00000000..a13e74e8
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/CommitsStore.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 {create} from "zustand";
+import {KaravanApi} from "@api/KaravanApi";
+import isEqual from "lodash/isEqual";
+
+export interface ProjectFileCommitDiff {
+    changeType: string;
+    newPath: string;
+    oldPath: string;
+    diff: string;
+    before: string;
+    after: string;
+}
+
+export interface ProjectFolderCommit {
+    id: string;
+    projectId: string;
+    authorName: string;
+    authorEmail: string;
+    commitTime: number;
+    message: string;
+    diffs: ProjectFileCommitDiff[];
+}
+
+export interface SystemCommit {
+    id: string;
+    authorName: string;
+    authorEmail: string;
+    commitTime: number;
+    message: string;
+    projectIds?: string[];
+}
+
+type CommitsState = {
+    projectCommits: ProjectFolderCommit[];
+    systemCommits: SystemCommit[];
+    clearProjectCommits: () => void;
+    fetchProjectCommits: (projectId: string) => Promise<void>;
+    fetchSystemCommits: () => Promise<void>;
+}
+
+export const useCommitsStore = create<CommitsState>((set, get) => ({
+    projectCommits: [],
+    systemCommits: [],
+    clearProjectCommits: (): void => {
+      set({projectCommits: []});
+    },
+    fetchProjectCommits: async (projectId: string): Promise<void> => {
+        await new Promise<ProjectFolderCommit[]>((resolve) => {
+            KaravanApi.getProjectCommits(projectId, resolve);
+        }).then(commits => {
+            const currentCommits = get().projectCommits;
+            if (!isEqual(currentCommits, commits)) {
+                set({ projectCommits: commits });
+            }
+        })
+    },
+    fetchSystemCommits: async (): Promise<void> => {
+        await new Promise<SystemCommit[]>((resolve) => {
+            KaravanApi.getSystemCommits(resolve);
+        }).then(systemCommits => {
+            const currentCommits = get().systemCommits;
+            if (!isEqual(currentCommits, systemCommits)) {
+                set({ systemCommits: systemCommits });
+            }
+        })
+    }
+}))
+
+
+
diff --git a/karavan-app/src/main/webui/src/stores/ComplexityStore.ts 
b/karavan-app/src/main/webui/src/stores/ComplexityStore.ts
new file mode 100644
index 00000000..9bd3fe38
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/ComplexityStore.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 {create} from "zustand";
+import isEqual from "lodash/isEqual";
+import {ComplexityProject} from "@models/ComplexityModels";
+import {ComplexityApi} from "@api/ComplexityApi";
+
+interface ComplexityState {
+    complexities: ComplexityProject[];
+    fetchComplexities: () => Promise<void>;
+}
+
+export const useComplexityStore = create<ComplexityState>((set, get) => ({
+    complexities: [],
+    fetchComplexities: async (): Promise<void> => {
+        const currentComplexities = get().complexities;
+        await new Promise<ComplexityProject[]>((resolve) => {
+            ComplexityApi.getComplexityProjects(resolve);
+        }).then(complexities=> {
+            if (!isEqual(currentComplexities, complexities)) {
+                set({complexities: complexities});
+            }
+        })
+    },
+}))
+
+
diff --git a/karavan-app/src/main/webui/src/stores/ContainerStatusesStore.ts 
b/karavan-app/src/main/webui/src/stores/ContainerStatusesStore.ts
new file mode 100644
index 00000000..f0fb0b46
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/ContainerStatusesStore.ts
@@ -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 {KaravanApi} from "@api/KaravanApi";
+import {create} from "zustand";
+import {ContainerStatus} from "@models/ProjectModels";
+import isEqual from "lodash/isEqual";
+import {AxiosResponse} from "axios";
+
+type ContainerStatusesState = {
+    containers: ContainerStatus[];
+    fetchContainers: () => Promise<void>;
+    fetchProjectContainers: (projectId: string) => Promise<void>;
+    findContainers: (projectId: string) => ContainerStatus[];
+}
+
+export const useContainerStatusesStore = create<ContainerStatusesState>((set, 
get) => ({
+    containers: [],
+    fetchContainers: async (): Promise<void> => {
+        const currentContainers = get().containers;
+        await new Promise<ContainerStatus[]>((resolve, reject) => {
+            KaravanApi.getAllContainerStatuses((containers: ContainerStatus[]) 
=> {
+                resolve(containers);
+            });
+        })
+            .then((containers) => {
+                if (!isEqual(currentContainers, containers)) {
+                    set({containers: containers});
+                }
+            });
+    },
+    fetchProjectContainers: async (projectId: string): Promise<void> => {
+        const currentContainers = get().containers;
+        await new Promise<AxiosResponse<ContainerStatus[]>>((resolve, reject) 
=> {
+            KaravanApi.getContainerStatus(projectId, (response: 
AxiosResponse<ContainerStatus[]>) => {
+                resolve(response);
+            });
+        })
+            .then((response) => {
+                if (!isEqual(currentContainers, response?.data ?? [])) {
+                    set({containers: response.data});
+                }
+            });
+    },
+    findContainers: (projectId: string) => {
+        const state = get();
+        return state.containers.filter(c => c.projectId === projectId);
+    },
+}))
+
diff --git a/karavan-app/src/main/webui/src/stores/DeploymentStatusesStore.ts 
b/karavan-app/src/main/webui/src/stores/DeploymentStatusesStore.ts
new file mode 100644
index 00000000..2394ee1d
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/DeploymentStatusesStore.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 {KaravanApi} from "@api/KaravanApi";
+import {create} from "zustand";
+import {DeploymentStatus} from "@models/ProjectModels";
+import isEqual from "lodash/isEqual";
+
+type DeploymentStatusesState = {
+    deployments: DeploymentStatus[];
+    fetchDeployments: () => Promise<void>;
+}
+
+export const useDeploymentStatusesStore = 
create<DeploymentStatusesState>((set, get) => ({
+    deployments: [],
+    fetchDeployments: async (): Promise<void> => {
+        const currentDeployments = get().deployments;
+        const deployments = await KaravanApi.getAllDeploymentStatuses();
+        if (!isEqual(currentDeployments, deployments)) {
+            set({ deployments });
+        }
+    }
+}))
+
diff --git a/karavan-app/src/main/webui/src/stores/DocumentationStore.ts 
b/karavan-app/src/main/webui/src/stores/DocumentationStore.ts
new file mode 100644
index 00000000..d1383cc2
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/DocumentationStore.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 {createWithEqualityFn} from "zustand/traditional";
+import {shallow} from "zustand/shallow";
+import {Component} from "@core/model/ComponentModels";
+import {ElementMeta} from "@core/model/CamelMetadata";
+
+interface DocumentationState {
+    isModalOpen: boolean;
+    setModalOpen: (isModalOpen: boolean) => void;
+    showBlocked: boolean;
+    setShowBlocked: (showBlocked: boolean) => void;
+    component?: Component;
+    setComponent: (component: Component) => void;
+    element?: ElementMeta;
+    setElement: (element: ElementMeta) => void;
+}
+
+export const useDocumentationStore = 
createWithEqualityFn<DocumentationState>((set) => ({
+    isModalOpen: false,
+    showBlocked: false,
+    setModalOpen: (isModalOpen: boolean) => {
+        set((state: DocumentationState) => {
+            return {isModalOpen: isModalOpen};
+        })
+    },
+    setShowBlocked: (showBlocked: boolean) => set({showBlocked}),
+    setComponent: (component: Component) => {
+        set((state: DocumentationState) => {
+            return {component: component};
+        })
+    },
+    setElement: (element: ElementMeta) => {
+        set((state: DocumentationState) => {
+            return {element: element};
+        })
+    },
+}), shallow)
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/stores/LogStore.ts 
b/karavan-app/src/main/webui/src/stores/LogStore.ts
new file mode 100644
index 00000000..447a0c9f
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/LogStore.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 {LogsEventBus} from "@bus/LogsEventBus";
+import {unstable_batchedUpdates} from "react-dom";
+import {createWithEqualityFn} from "zustand/traditional";
+import {shallow} from "zustand/shallow";
+
+const MAX_LOG_LINES = 1000;
+
+interface LogState {
+    podName?: string,
+    data: string[];
+    setData: (data: string[]) => void;
+}
+
+export const useLogStore = createWithEqualityFn<LogState>((set) => ({
+    podName: undefined,
+    data: [],
+    setData: (data: string[]) => {
+        set({data: data})
+    }
+}), shallow)
+
+const sub = LogsEventBus.onLog()?.subscribe((result: ["add" | "set", string]) 
=> {
+    if (result[0] === 'add') {
+        unstable_batchedUpdates(() => {
+            useLogStore.setState((state: LogState) => {
+                const newEntry = result[1]?.length !== 0 ? result[1] : "\n";
+
+                // Combine, then slice from the end (negative index)
+                const newData = [...state.data, newEntry];
+                const trimmedData = newData.length > MAX_LOG_LINES
+                    ? newData.slice(-MAX_LOG_LINES)
+                    : newData;
+
+                return { data: trimmedData };
+            })
+        })
+    } else if (result[0] === 'set') {
+        unstable_batchedUpdates(() => {
+            useLogStore.setState({data: [result[1]]});
+        })
+    }
+});
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/stores/ProjectStore.ts 
b/karavan-app/src/main/webui/src/stores/ProjectStore.ts
new file mode 100644
index 00000000..cc468f0a
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/ProjectStore.ts
@@ -0,0 +1,552 @@
+/*
+ * 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 {
+    AppConfig,
+    CamelStatus,
+    ContainerImage,
+    DesignerTab,
+    FileOperation,
+    Project,
+    ProjectCommited,
+    ProjectFile,
+    ProjectFileCommited,
+    ProjectOperation,
+    ServiceStatus
+} from "@models/ProjectModels";
+import {createWithEqualityFn} from "zustand/traditional";
+import {shallow} from "zustand/shallow";
+import {KaravanApi} from "@api/KaravanApi";
+import {AxiosResponse} from "axios";
+import isEqual from "lodash/isEqual";
+import {upsertFile} from "@utils/FileUtils";
+
+interface AppConfigState {
+    loading: boolean;
+    setLoading: (loading: boolean) => void;
+    config: AppConfig;
+    setConfig: (config: AppConfig) => void;
+    selectedEnv: string[];
+    setSelectedEnv: (selectedEnv: string[]) => void;
+    selectEnvironment: (name: string, selected: boolean) => void;
+    dockerInfo: any;
+    setDockerInfo: (info: any) => void;
+}
+
+export const useAppConfigStore = createWithEqualityFn<AppConfigState>((set) => 
({
+    loading: false,
+    setLoading: (loading: boolean) => {
+        set({loading: loading})
+    },
+    config: new AppConfig(),
+    setConfig: (config: AppConfig) => {
+        set({config: config})
+    },
+    selectedEnv: [],
+    setSelectedEnv: (selectedEnv: string[]) => {
+        set((state: AppConfigState) => {
+            state.selectedEnv.length = 0;
+            state.selectedEnv.push(...selectedEnv);
+            return {selectedEnv: state.selectedEnv};
+        });
+    },
+    selectEnvironment(name: string, selected: boolean) {
+        set((state: AppConfigState) => {
+            if (selected && !state.selectedEnv.includes(name)) {
+                state.selectedEnv.push(name);
+            } else if (!selected && state.selectedEnv.includes(name)) {
+                const filtered = state.selectedEnv.filter(e => e !== name);
+                state.selectedEnv.length = 0;
+                state.selectedEnv.push(...filtered);
+            }
+            return {selectedEnv: state.selectedEnv};
+        });
+    },
+    dockerInfo: {},
+    setDockerInfo: (info: any)  => {
+        set({
+            dockerInfo: info
+        })
+    },
+}), shallow)
+
+export const PROJECT_WITH_NO_LABELS = "zzzPROJECT_WITH_NO_LABELS";
+const STORAGE_KEY_PROJECTS_SELECTED_LABELS = "projects-selected-labels";
+function getInitialProjectLabels(): string[] {
+    if (typeof window === "undefined") return [];
+    const saved = localStorage.getItem(STORAGE_KEY_PROJECTS_SELECTED_LABELS);
+    if (saved === null) return [];
+    return JSON.parse(saved);
+}
+
+interface ProjectsState {
+    projects: Project[];
+    projectsCommited: ProjectCommited[];
+    setProjects: (projects: Project[]) => void;
+    upsertProject: (project: Project) => void;
+    fetchProjects: () => Promise<void>;
+    fetchProjectLabels: () => Promise<void>;
+    fetchProjectsCommited: () => Promise<void>;
+    filter: string;
+    setFilter: (filter: string) => void;
+    projectLabels: any;
+    labels: string[];
+    selectedLabels: string[];
+    setSelectedLabels: (selectedLabels: string[]) => void;
+}
+
+export const useProjectsStore = createWithEqualityFn<ProjectsState>((set, get) 
=> ({
+    projects: [],
+    projectsCommited: [],
+    projectLabels: {},
+    labels: [],
+    selectedLabels: getInitialProjectLabels(),
+    setProjects: (ps: Project[]) => {
+        set((state: ProjectsState) => ({
+            projects: ps,
+        }));
+    },
+    setSelectedLabels: (selectedLabels: string[]) => {
+        localStorage.setItem(STORAGE_KEY_PROJECTS_SELECTED_LABELS, 
JSON.stringify(selectedLabels));
+        set({selectedLabels: selectedLabels});
+    },
+    fetchProjects: async (): Promise<void> => {
+        await new Promise<Project[]>((resolve) => {
+            KaravanApi.getProjects(resolve);
+        }).then(projects => {
+            set({ projects: projects });
+        })
+    },
+    fetchProjectLabels: async (): Promise<void> => {
+        await new Promise<any>((resolve) => {
+            KaravanApi.getProjectsLabels(resolve);
+        }).then(projectLabels => {
+            const normalizedLabels = Object.fromEntries(
+                Object.entries(projectLabels).map(([projectId, labels]) => {
+                    const hasLabels = Array.isArray(labels) && labels.length > 
0;
+                    return [projectId, hasLabels ? labels : 
[PROJECT_WITH_NO_LABELS]];
+                })
+            );
+            const labels = Array.from(new Set((Object.values(projectLabels) as 
string[][]).flat()));
+            set({ projectLabels: normalizedLabels, labels: [...labels, 
PROJECT_WITH_NO_LABELS] });
+        })
+    },
+    fetchProjectsCommited: async (): Promise<void> => {
+        const currentProjectsCommited = get().projectsCommited;
+        await new Promise<ProjectCommited[]>((resolve) => {
+            KaravanApi.getProjectsCommited(resolve);
+        }).then(projectsCommited => {
+            if (!isEqual(currentProjectsCommited, projectsCommited)) {
+                set({ projectsCommited: projectsCommited });
+            }
+        })
+    },
+    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]
+        }));
+    },
+    filter: '',
+    setFilter: (filter: string) => {
+        set({filter: filter});
+    }
+}), shallow)
+
+
+export const ProjectMenus = ['topology', 'source', 'build', 'containers'] as 
const;
+export const ProjectRuntimeMenus = ['runtime', 'log', 'trace', 'tryout'] as 
const;
+export type ProjectMenu = typeof ProjectMenus[number];
+export type ProjectRuntimeMenu = typeof ProjectRuntimeMenus[number];
+
+interface ProjectState {
+    isPulling: boolean,
+    isPushing: boolean,
+    images: ContainerImage [],
+    setImages: (images: ContainerImage []) => void;
+    project: Project;
+    setProject: (project: Project, operation: ProjectOperation) => void;
+    operation: "create" | "select" | "delete" | "none" | "copy";
+    tabIndex: ProjectMenu | ProjectRuntimeMenu;
+    setTabIndex: (tabIndex: ProjectMenu | ProjectRuntimeMenu | number) => void;
+    setOperation: (o: ProjectOperation) => void;
+    camelStatuses: CamelStatus[],
+    fetchCamelStatuses: (projectId: string) => Promise<void>;
+    camelTraces: CamelStatus[],
+    setCamelTraces: (camelTraces: CamelStatus[]) => void;
+    refreshTrace: boolean
+    setRefreshTrace: (refreshTrace: boolean) => void;
+}
+
+export const useProjectStore = createWithEqualityFn<ProjectState>((set, get) 
=> ({
+    project: new Project(),
+    images: [],
+    operation: 'none',
+    tabIndex: ProjectMenus[0],
+    isPushing: false,
+    isPulling: false,
+    setProject: (project: Project, operation: ProjectOperation) => {
+        set((state: ProjectState) => ({
+            project: project,
+            operation: operation,
+            refreshTrace: false,
+            jvm: {},
+            context: {},
+            trace: {},
+            memory: {},
+            tabIndex: state.tabIndex
+        }));
+    },
+    setOperation: (o: ProjectOperation) => {
+        set((state: ProjectState) => ({
+            operation: o
+        }));
+    },
+    setTabIndex: (tabIndex: ProjectMenu | ProjectRuntimeMenu | number) => {
+        const tab = typeof tabIndex === 'number' ? ProjectMenus[tabIndex] : 
tabIndex;
+        set({tabIndex: tab});
+    },
+    setImages: (images: ContainerImage[]) => {
+        set((state: ProjectState) => {
+            state.images.length = 0;
+            state.images.push(...images);
+            return {images: state.images};
+        });
+    },
+    camelStatuses: [],
+    fetchCamelStatuses: async (projectId: string): Promise<void> => {
+        const currentStatuses = get().camelStatuses;
+        await new Promise<AxiosResponse<CamelStatus[]>>((resolve) => {
+            KaravanApi.getProjectCamelStatuses(projectId, resolve);
+        }).then(response=> {
+            const statuses = response.status === 200 ? response.data : [];
+            if (!isEqual(currentStatuses, statuses)) {
+                set({camelStatuses: statuses});
+            }
+        })
+    },
+    camelTraces: [],
+    setCamelTraces: (camelTraces: CamelStatus[]) => {
+        set((state: ProjectState) => {
+            return {camelTraces: camelTraces};
+        });
+    },
+    refreshTrace: false,
+    setRefreshTrace: (refreshTrace: boolean) => {
+        set({refreshTrace: refreshTrace})
+    },
+}), shallow)
+
+
+export type FilesSideBarType = 'create' | 'upload' | 'library'
+
+interface FilesState {
+    files: ProjectFile[];
+    commitedFiles: ProjectFileCommited[];
+    diff: any;
+    setFiles: (files: ProjectFile[]) => void;
+    fetchFiles: (projectId: string) => Promise<ProjectFile[]>;
+    fetchFile: (projectId: string, fileName: string) => Promise<ProjectFile>;
+    deleteFile: (projectId: string, fileName: string) => Promise<void>;
+    saveFile: (file: ProjectFile, create?: boolean) => Promise<void>;
+    fetchCommitedFiles: (projectId: string) => Promise<any>;
+    upsertFile: (file: ProjectFile) => void;
+    selectedFileNames: string[];
+    setSelectedFileNames: (selectedFileNames: string[]) => void;
+    selectFile: (filename: string) => void;
+    unselectFile: (filename: string) => void;
+    selector: 'files' | 'commits';
+    setSelector: (selector: 'files' | 'commits') => void;
+    showSideBar: FilesSideBarType;
+    setShowSideBar: (showSideBar: FilesSideBarType) => void;
+    title: string;
+    setTitle: (title: string) => void;
+    backgroundSaveFile: (file: ProjectFile) => Promise<void>;
+}
+
+export const useFilesStore = createWithEqualityFn<FilesState>((set, get) => ({
+    files: [],
+    commitedFiles: [],
+    diff: {},
+    selectedFileNames: [],
+    selector: 'files',
+    showSideBar: null,
+    backgroundSaveFile: async (file: ProjectFile) => {
+        // Optimistically update the store
+        get().upsertFile(file);
+        useFileStore.getState().setFile('select', file);
+
+        // Perform API call in background
+        KaravanApi.putProjectFile(file, res => {
+            if (res.status !== 200) {
+                // Handle error, maybe revert state or show toast
+            }
+        });
+    },
+    setShowSideBar: (showSideBar: FilesSideBarType) => {
+        set({showSideBar: showSideBar});
+    },
+    title: null,
+    setTitle: (title: string) => {
+        set({ title: title });
+    },
+    setFiles: (files: ProjectFile[]) => {
+        set((state: FilesState) => ({
+            files: files
+        }));
+    },
+    fetchFiles: async (projectId: string): Promise<any> => {
+        const currentFiles = get().files;
+        return await new Promise<ProjectFile[]>((resolve) => {
+            KaravanApi.getFiles(projectId, resolve);
+        }).then(files => {
+            if (!isEqual(currentFiles, files)) {
+                set({files: files});
+            }
+            return files;
+        })
+    },
+    fetchFile: async (projectId: string, fileName: string): 
Promise<ProjectFile> => {
+        const currentFiles = get().files;
+        return await new Promise<ProjectFile>((resolve) => {
+            KaravanApi.getProjectFilesByName(projectId, fileName, resolve);
+        }).then(file => {
+            console.log(file)
+            if (file !== undefined) {
+                const oldFiles: ProjectFile[] = currentFiles.filter(f => 
f.name !== fileName);
+                set({files: [...oldFiles, file]});
+            }
+            return file;
+        })
+    },
+    deleteFile: async (projectId: string, fileName: string): Promise<void> => {
+        const currentFiles = get().files;
+        return await new Promise<any>((resolve) => {
+            KaravanApi.deleteProjectFileByName(projectId, fileName, resolve);
+        }).then(file => {
+            const oldFiles: ProjectFile[] = currentFiles.filter(f => f.name 
!== fileName);
+            set({ files: [...oldFiles] });
+            return file;
+        })
+    },
+    fetchCommitedFiles: async (projectId: string): Promise<any> => {
+        const currentCommitedFiles = get().commitedFiles;
+        await new Promise<ProjectFileCommited[]>((resolve) => {
+            KaravanApi.getCommitedFiles(projectId, resolve);
+        }).then(files => {
+            if (!isEqual(currentCommitedFiles, files)) {
+                set({ commitedFiles: files });
+            }
+        })
+    },
+    saveFile: async (file: ProjectFile, create: boolean = false) => {
+        const prevFiles = get().files;
+        const newFiles = upsertFile(prevFiles, file);
+        set({ files: newFiles });
+
+        await new Promise<{ result: AxiosResponse | any; file: ProjectFile | 
any }>((resolve, reject) => {
+            if (create) {
+                KaravanApi.saveProjectFile(file, (result, file) => {
+                    if (result) {
+                        resolve({result, file});
+                    } else {
+                        reject(new Error("API returned failure result."));
+                    }
+                });
+            } else {
+                KaravanApi.putProjectFile(file, (result) => {
+                    if (result) {
+                        resolve({result, file});
+                    } else {
+                        reject(new Error("API returned failure result."));
+                    }
+                });
+            }
+        }).catch(error => {
+            set({ files: prevFiles });
+            console.error(error);
+        });
+    },
+    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]
+        }));
+    },
+    setSelectedFileNames: (selectedFileNames: string[]) => {
+        set((state: FilesState) => ({
+            selectedFileNames: selectedFileNames
+        }));
+    },
+    selectFile: (filename: string) => {
+        set((state: FilesState) => {
+            const names = [...state.selectedFileNames];
+            if (!state.selectedFileNames.includes(filename)) {
+                names.push(filename);
+            }
+            return ({selectedFileNames: names})
+        });
+    },
+    unselectFile: (filename: string) => {
+        set((state: FilesState) => {
+            const names = [...state.selectedFileNames.filter(f => f !== 
filename)];
+            return ({selectedFileNames: names})
+        });
+    },
+    setSelector: (selector: 'files' | 'commits') => {
+        set({selector: selector});
+    },
+}), shallow)
+
+interface FileState {
+    file?: ProjectFile;
+    fileCommited?: ProjectFile;
+    operation: FileOperation;
+    designerTab?: DesignerTab;
+    setFile: (operation: FileOperation, file?: ProjectFile, designerTab?: 
DesignerTab) => void;
+    fetchCommitedFile: (file: ProjectFile) => Promise<void>
+    undoFile: () => Promise<void>
+}
+
+export const useFileStore = createWithEqualityFn<FileState>((set, get) => ({
+    file: undefined,
+    operation: "none",
+    designerTab: undefined,
+    addProperty: '',
+    setFile: (operation: FileOperation, file?: ProjectFile, designerTab?: 
DesignerTab) => {
+        set((state: FileState) => ({
+            file: file,
+            operation: operation,
+            designerTab: designerTab
+        }));
+    },
+    fetchCommitedFile: async (file: ProjectFile): Promise<any> => {
+        await new Promise<ProjectFile>((resolve) => {
+            KaravanApi.getFileCommited(file?.projectId, file?.name, resolve);
+        }).then(file => {
+            if (!isEqual(get().fileCommited, file)) {
+                set({ fileCommited: file });
+            }
+        })
+    },
+    undoFile: async (): Promise<any> => {
+        await new Promise<AxiosResponse>((resolve) => {
+            KaravanApi.putProjectFile(get().fileCommited, resolve);
+        }).then(result => {
+            if (result.status === 200 && !isEqual(get().fileCommited, 
get().file)) {
+                set({ file: get().fileCommited });
+            }
+        })
+    },
+}), shallow)
+
+interface WizardState {
+    showWizard: boolean;
+    setShowWizard: (showWizard: boolean) => void;
+}
+
+export const useWizardStore = createWithEqualityFn<WizardState>((set) => ({
+    showWizard: false,
+    setShowWizard: (showWizard: boolean) => {
+        set({showWizard: showWizard})
+    },
+}), shallow)
+
+interface DevModeState {
+    podName?: string,
+    status: "none" | "wip",
+    setStatus: (status: "none" | "wip") => void,
+    setPodName: (podName?: string) => void,
+}
+
+export const useDevModeStore = createWithEqualityFn<DevModeState>((set) => ({
+    podName: undefined,
+    status: "none",
+    setStatus: (status: "none" | "wip") => {
+        set((state: DevModeState) => ({
+            status: status,
+        }));
+    },
+    setPodName: (podName?: string) => {
+        set((state: DevModeState) => ({
+            podName: podName,
+        }));
+    },
+}), shallow)
+
+interface StatusesState {
+    services: ServiceStatus[];
+    camelContexts: CamelStatus[];
+    routes: CamelStatus[];
+    setRoutes: (routes: CamelStatus[]) => void;
+    consumers: CamelStatus[];
+    setConsumers: (consumers: CamelStatus[]) => void;
+    processors: CamelStatus[];
+    setProcessors: (processors: CamelStatus[]) => void;
+    setServices: (s: ServiceStatus[]) => void;
+    setCamelContexts: (camelContexts: CamelStatus[]) => void;
+    camels: CamelStatus[];
+    setCamels: (c: CamelStatus[]) => void;
+}
+
+export const useStatusesStore = createWithEqualityFn<StatusesState>((set, get) 
=> ({
+    services: [],
+    camelContexts: [],
+    routes: [],
+    consumers: [],
+    processors: [],
+    setServices: (s: ServiceStatus[]) => {
+        set((state: StatusesState) => ({
+            services: s,
+        }));
+    },
+    setCamelContexts: (c: CamelStatus[]) => {
+        set((state: StatusesState) => ({
+            camelContexts: c,
+        }));
+    },
+    setRoutes: (routes: CamelStatus[]) => {
+        set((state: StatusesState) => ({
+            routes: routes,
+        }));
+    },
+    setConsumers: (consumers: CamelStatus[]) => {
+        set({consumers})
+    },
+    setProcessors: (processors: CamelStatus[]) => {
+        set({processors})
+    },
+    camels: [],
+    setCamels: (c: CamelStatus[]) => {
+        set((state: StatusesState) => ({
+            camels: c,
+        }));
+    },
+}), shallow)
+
+
+interface SelectedContainerState {
+    selectedContainerName?: string;
+    setSelectedContainerName: (selectedContainerName?: string) => void;
+}
+
+export const useSelectedContainerStore = 
createWithEqualityFn<SelectedContainerState>((set) => ({
+    setSelectedContainerName: (selectedContainerName?: string)  => {
+        set({selectedContainerName: selectedContainerName })
+    },
+}), shallow)
diff --git a/karavan-app/src/main/webui/src/stores/ReadinessStore.ts 
b/karavan-app/src/main/webui/src/stores/ReadinessStore.ts
new file mode 100644
index 00000000..07b47c86
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/ReadinessStore.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 isEqual from 'lodash/isEqual';
+import {KaravanApi} from "@api/KaravanApi";
+import {create} from "zustand";
+
+type ReadinessState = {
+    readiness: any;
+    fetchReadiness: () => Promise<void>;
+}
+
+export const useReadinessStore = create<ReadinessState>((set, get) => ({
+    readiness: undefined,
+    fetchReadiness: async (): Promise<void> => {
+        await new Promise<any>((resolve, reject) => {
+            KaravanApi.getReadiness(resolve); // resolve the promise when data 
is available
+        })
+            .then((readiness) => {
+                const currentReadiness = get().readiness;
+                if (!isEqual(currentReadiness, readiness)) {
+                    set({ readiness: readiness });
+                }
+            });
+    }
+}))
+
diff --git a/karavan-app/src/main/webui/src/stores/SearchStore.ts 
b/karavan-app/src/main/webui/src/stores/SearchStore.ts
new file mode 100644
index 00000000..93ccd098
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/SearchStore.ts
@@ -0,0 +1,21 @@
+import {createWithEqualityFn} from "zustand/traditional";
+import {shallow} from "zustand/shallow";
+import {SearchResult} from "@models/SearchModels";
+
+interface SearchState {
+    search: string;
+    setSearch: (search: string) => void;
+    searchResults: SearchResult[];
+    setSearchResults: (searchResult: SearchResult[]) => void;
+}
+
+export const useSearchStore = createWithEqualityFn<SearchState>((set) => ({
+    search: '',
+    searchResults: [],
+    setSearch: (search: string)  => {
+        set({search: search})
+    },
+    setSearchResults: (searchResults: SearchResult[])  => {
+        set({searchResults: searchResults})
+    },
+}), shallow)
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/stores/SettingsStore.ts 
b/karavan-app/src/main/webui/src/stores/SettingsStore.ts
new file mode 100644
index 00000000..be1be421
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/SettingsStore.ts
@@ -0,0 +1,72 @@
+import {ProjectFile, ProjectType} from "@models/ProjectModels";
+import {create} from "zustand";
+import {KaravanApi} from "@api/KaravanApi";
+import isEqual from "lodash/isEqual";
+import concat from "lodash/concat";
+
+export const SettingsMenus = ['templates', 'kamelets', 'configuration'] as 
const;
+export type SettingsMenu = typeof SettingsMenus[number];
+
+type SettingsState = {
+    currentMenu: SettingsMenu;
+    setCurrentMenu: (currentMenu: SettingsMenu) => void;
+    selectedProjectId?: string;
+    setSelectedProjectId: (selectedProjectId?: string) => void;
+    selectedFileName?: string;
+    setSelectedFilename: (selectedFileName?: string) => void;
+}
+
+export const useSettingsStore = create<SettingsState>((set, get) => ({
+    currentMenu: SettingsMenus[0],
+    setCurrentMenu: (currentMenu: SettingsMenu) => {
+        set({ currentMenu: currentMenu });
+    },
+    setSelectedProjectId: (selectedProjectId: string) => {
+        set({ selectedProjectId: selectedProjectId });
+    },
+    setSelectedFilename: (selectedFileName: string) => {
+        set({ selectedFileName: selectedFileName });
+    },
+}))
+
+type TemplatesState = {
+    templateFiles: ProjectFile[];
+    saveTemplateFile: (file: ProjectFile) => Promise<void>;
+    fetchTemplateFiles: () => Promise<void>;
+}
+
+export const useTemplatesStore = create<TemplatesState>((set, get) => ({
+    templateFiles: [],
+    fetchTemplateFiles: async (): Promise<void> => {
+        await new Promise<ProjectFile[]>((resolve) => {
+            KaravanApi.getFiles(ProjectType.templates, resolve);
+        }).then(templates => {
+            const currentTemplateFiles = get().templateFiles;
+            if (!isEqual(currentTemplateFiles, templates)) {
+                set({ templateFiles: templates });
+            }
+        })
+    },
+    saveTemplateFile: async (file: ProjectFile) => {
+        const prevSettings = [...get().templateFiles];
+        const newTemplateFiles = concat(prevSettings.filter(f => f.name !== 
file.name), file)
+        set({ templateFiles: newTemplateFiles });
+
+        await new Promise<{ result: boolean; file: ProjectFile | any 
}>((resolve, reject) => {
+            KaravanApi.saveProjectFile(file, (result, file) => {
+                if (result) {
+                    resolve({ result, file });
+                } else {
+                    // Reject the promise if the API explicitly returns 
result: false (Application error)
+                    reject(new Error("API returned failure result."));
+                }
+            });
+        }).catch(error => {
+            set({ templateFiles: prevSettings });
+            console.error(error);
+        });
+    }
+}))
+
+
+
diff --git a/karavan-app/src/main/webui/src/stores/SystemStore.ts 
b/karavan-app/src/main/webui/src/stores/SystemStore.ts
new file mode 100644
index 00000000..bd3ea094
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/SystemStore.ts
@@ -0,0 +1,51 @@
+import {createWithEqualityFn} from "zustand/traditional";
+import {shallow} from "zustand/shallow";
+import {KubernetesConfigMap, KubernetesSecret} from "@models/SystemModels";
+
+export const SystemMenus = ['containers', 'deployments', 'secrets', 
'configMaps', 'envVars', 'appProps', 'log'] as const;
+export type SystemMenu = typeof SystemMenus[number] ;
+
+interface SystemState {
+    filter: string;
+    setFilter: (filter: string) => void;
+    secrets: KubernetesSecret[];
+    setSecrets: (secrets: KubernetesSecret[]) => void;
+    configmaps: KubernetesConfigMap[];
+    setConfigMaps: (configmaps: KubernetesConfigMap[]) => void;
+    tabIndex: SystemMenu;
+    setTabIndex: (tabIndex: SystemMenu | number) => void;
+    envVars: string[];
+    setEnvVars: (envVars: string[]) => void;
+    appProps: string[];
+    setAppProps: (appProps: string[]) => void;
+}
+
+export const useSystemStore = createWithEqualityFn<SystemState>((set) => ({
+    filter: '',
+    secrets: [],
+    configmaps: [],
+    tabIndex: 'containers',
+    setFilter: (filter: string)=> {
+        set({filter: filter});
+    },
+    setSecrets: (secrets: KubernetesSecret[])=> {
+        set({secrets: secrets});
+    },
+    setConfigMaps: (configmaps: KubernetesConfigMap[]) => {
+        set({configmaps: configmaps});
+    },
+    setTabIndex: (tabIndex: SystemMenu | number) => {
+        const tab = typeof tabIndex === 'number' ? SystemMenus[tabIndex] : 
tabIndex;
+        set({tabIndex: tab});
+    },
+    envVars: [],
+    appProps: [],
+    setEnvVars: (envVars: string[]) => {
+        set({envVars: envVars});
+    },
+    setAppProps: (appProps: string[]) => {
+        set({appProps: appProps});
+    }
+}), shallow)
+
+
diff --git a/karavan-app/src/main/webui/src/stores/useKubernetesStore.ts 
b/karavan-app/src/main/webui/src/stores/useKubernetesStore.ts
new file mode 100644
index 00000000..ddf8d2e8
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/useKubernetesStore.ts
@@ -0,0 +1,28 @@
+import {create} from "zustand";
+import isEqual from "lodash/isEqual";
+import {PodEvent} from "@models/ProjectModels";
+import {KubernetesApi} from "@api/KubernetesApi";
+
+interface KubernetesState {
+    podEvents: PodEvent[];
+
+    fetchPodEvents: (containerName: string) => Promise<void>;
+    clearPodEvents: () => void;
+}
+
+export const useKubernetesStore = create<KubernetesState>((set, get) => ({
+    podEvents: [],
+    fetchPodEvents: async (containerName: string): Promise<void> => {
+        try {
+            const podEvents: PodEvent[] = await 
KubernetesApi.getPodEvents(containerName);
+            if (!isEqual(get().podEvents, podEvents)) {
+                set({podEvents: podEvents});
+            }
+        } catch (error) {
+            console.error("Failed to fetch reports for project", error);
+        }
+    },
+    clearPodEvents: () => {
+        set({podEvents: []})
+    }
+}));
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/stores/useUIStore.ts 
b/karavan-app/src/main/webui/src/stores/useUIStore.ts
new file mode 100644
index 00000000..c98cc007
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/useUIStore.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 {create} from "zustand";
+import {KaravanApi} from "@api/KaravanApi";
+import {ProjectFile, ProjectType} from "@models/ProjectModels";
+
+interface UIState {
+    pageId?: string;
+    setPageId: (pageId?: string) => void;
+    customLogo?: string;
+    customName?: string;
+    fetchBrand: () => Promise<void>;
+}
+
+export const useUIStore = create<UIState>((set, get) => ({
+    pageId: undefined,
+    setPageId: (pageId?: string) => set({pageId: pageId}),
+    customLogo: undefined,
+    fetchBrand: async (): Promise<void> => {
+        const logoP = new Promise<ProjectFile>((resolve) => {
+            KaravanApi.getProjectFilesByName(ProjectType.configuration, 
'logo.svg', resolve)
+        });
+
+        const nameP = new Promise<ProjectFile>((resolve) => {
+            KaravanApi.getProjectFilesByName(ProjectType.configuration, 
'name.svg', resolve)
+        });
+
+        // Wait for BOTH API calls
+        const [logo, name] = await Promise.all([logoP, nameP]);
+        set({customLogo: logo?.code, customName: name?.code});
+    }
+}))
\ No newline at end of file

Reply via email to