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 2026/08/25 15:19:44 UTC
(camel-karavan) 19/42: Karavan Webapp UI Stores
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 <ma...@talismancloud.io>
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