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
