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 467fd594d309a60bab55346bb3e421954f9bc796 Author: Marat Gubaidullin <[email protected]> AuthorDate: Mon Aug 24 17:58:13 2026 -0400 Karavan-app UI Page Compass --- karavan-app/src/main/webui/src/ui/compass/App.css | 25 +++ karavan-app/src/main/webui/src/ui/compass/App.tsx | 117 +++++++++++++ .../src/main/webui/src/ui/compass/AppCompass.css | 42 +++++ .../src/main/webui/src/ui/compass/AppCompass.tsx | 67 +++++++ .../src/main/webui/src/ui/compass/AppDock.css | 44 +++++ .../src/main/webui/src/ui/compass/AppDock.tsx | 192 +++++++++++++++++++++ .../src/main/webui/src/ui/compass/AppFooter.css | 4 + .../src/main/webui/src/ui/compass/AppFooter.tsx | 43 +++++ .../src/main/webui/src/ui/compass/AppMain.css | 6 + .../src/main/webui/src/ui/compass/AppMain.tsx | 26 +++ .../main/webui/src/ui/compass/AppNavigation.tsx | 36 ++++ .../main/webui/src/ui/compass/ReadinessPanel.tsx | 74 ++++++++ .../webui/src/ui/compass/navigation/MainRoutes.tsx | 89 ++++++++++ .../src/ui/compass/navigation/NavigationMenu.tsx | 52 ++++++ .../ui/compass/navigation/NotAuthorizedPage.tsx | 26 +++ .../src/ui/compass/navigation/PageFallback.tsx | 13 ++ .../src/ui/compass/navigation/PlatformLogo.tsx | 32 ++++ .../src/ui/compass/navigation/ProtectedRoute.tsx | 32 ++++ .../main/webui/src/ui/compass/navigation/Routes.ts | 15 ++ .../webui/src/ui/compass/theme/DarkModeToggle.tsx | 28 +++ .../webui/src/ui/compass/theme/ThemeContext.tsx | 50 ++++++ .../main/webui/src/ui/compass/useCompassStore.ts | 59 +++++++ .../src/main/webui/src/ui/compass/useConfig.tsx | 29 ++++ .../src/main/webui/src/ui/compass/useMainHook.tsx | 153 ++++++++++++++++ 24 files changed, 1254 insertions(+) diff --git a/karavan-app/src/main/webui/src/ui/compass/App.css b/karavan-app/src/main/webui/src/ui/compass/App.css new file mode 100644 index 00000000..d246edeb --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/App.css @@ -0,0 +1,25 @@ +.root-main { + display: flex; + flex-direction: row; + height: 100%; +} + +.karavan .loading-page { + width: 100%; +} +.karavan .loading-page .spinner { + position: absolute; +} +.karavan .loading-page .logo-placeholder { + position: absolute; + height: 100px; +} + +.karavan .loading-page .logo { + height: 100px; +} + +:root .pf-v6-c-compass__nav-main { + padding-inline-start: var(--pf-t--global--spacer--md); + padding-inline-end: var(--pf-t--global--spacer--md); +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/App.tsx b/karavan-app/src/main/webui/src/ui/compass/App.tsx new file mode 100644 index 00000000..a9fef72a --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/App.tsx @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, {useContext, useEffect, useRef} from "react"; +import {useMainHook} from "@compass/useMainHook"; +import {Notification} from "@designer/utils/Notification"; +import {NotificationApi} from "@api/NotificationApi"; +import {AuthContext} from "@api/auth/AuthProvider"; +import {AuthApi, getCurrentUser} from "@api/auth/AuthApi"; +import {PLATFORM_DEVELOPER} from "@models/AccessModels"; +import {ReadinessPanel} from "@compass/ReadinessPanel"; +import {useReadinessStore} from "@stores/ReadinessStore"; +import {useNavigate} from "react-router-dom"; +import {useUIStore} from "@stores/useUIStore"; +import {ErrorEventBus} from "@bus/ErrorEventBus"; +import {ROUTES} from "@compass/navigation/Routes"; +import {LoginPage} from "@login/LoginPage"; +import AppCompass from "@compass/AppCompass"; +import '@compass/App.css'; +import {useGlobalShortcuts} from "@command-palette/CommandEventBus"; + + +export function App() { + + const {readiness} = useReadinessStore(); + const {fetchBrand, customLogo} = useUIStore(); + const controllerRef = useRef(new AbortController()); + const {getData, showApplication} = useMainHook(); + const show = showApplication(); + const { user, loading, authType } = useContext(AuthContext); + const navigate = useNavigate(); + useGlobalShortcuts(); + + useEffect(() => { + const interval = setInterval(() => resetNotification(), 60000); + const sub = ErrorEventBus.onApiError()?.subscribe(err => { + console.log("ApiError", err?.config?.url, err) + if (err?.response?.status === 401 && AuthApi.authType === 'session') { + navigate(ROUTES.LOGIN); + window.location.reload(); + } + }); + return () => { + clearInterval(interval); + sub?.unsubscribe(); + }; + }, []); + + useEffect(() => { + if (showMain()) { + getData(); + resetNotification(); + } + }, [readiness, user]); + + useEffect(() => { + if (user && customLogo === undefined) { + fetchBrand(); + } + }, [readiness, user]); + + function resetNotification() { + try { + controllerRef.current.abort() + const controller = new AbortController(); + controllerRef.current = controller; + NotificationApi.notification(controller); + } catch (e) { + console.error(e); + } + } + + function showMain() { + return AuthApi.authType !== undefined && readiness?.status === true; + } + + function isViewer(){ + return getCurrentUser()?.roles?.includes(PLATFORM_DEVELOPER); + } + + // The username/password LoginPage belongs to the 'session' auth type only. + // In 'oidc' mode SsoApi redirects the browser to Keycloak (onLoad: 'login-required'), + // so rendering it while that redirect is in flight only produces a flash. + function showLoginPage() { + return !user && !loading && authType === 'session'; + } + + if (show) { + return ( + <> + <ReadinessPanel/> + {user && <AppCompass/>} + {showLoginPage() && <LoginPage/>} + <Notification/> + </> + ) + } else { + return ( + <div className={isViewer() ? "viewer-group root-main karavan" : "root-main karavan"}> + {<ReadinessPanel/>} + </div> + ) + } +} diff --git a/karavan-app/src/main/webui/src/ui/compass/AppCompass.css b/karavan-app/src/main/webui/src/ui/compass/AppCompass.css new file mode 100644 index 00000000..6935f772 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppCompass.css @@ -0,0 +1,42 @@ +.pf-v6-c-compass.pf-m-docked .pf-v6-c-compass__container { + grid-template-areas: "dock main"; + grid-template-rows: auto; + grid-template-columns: auto 1fr; + row-gap: var(--pf-v6-c-compass__main--RowGap); + align-items: stretch; + padding: 0; +} + +:root { + .pf-v6-c-compass__dock .pf-v6-c-nav__item::before { + background-color: var(--platform-color); + } + .pf-v6-c-compass__nav .pf-v6-c-tabs.pf-m-animate-current:not(.pf-m-box) .pf-v6-c-tabs__list::after { + border: 1px solid var(--platform-color); + } + .pf-v6-c-compass__logo .pf-v6-c-tabs.pf-m-animate-current:not(.pf-m-box) .pf-v6-c-tabs__list::after { + border: 1px solid var(--platform-color); + } + .pf-v6-c-tabs.pf-m-animate-current:not(.pf-m-box) .pf-v6-c-tabs__list::after { + border: 1px solid var(--platform-color); + } +} + +.pf-v6-c-compass.pf-m-docked .pf-v6-c-compass__main { + padding: var(--pf-t--global--spacer--md); + gap: var(--pf-t--global--spacer--100); +} + +.pf-v6-c-compass .pf-topology-content { + +} + +.pf-v6-c-compass .infra-icon-k8s, +.pf-v6-c-compass .infra-icon-docker { + fill: #4bb9ec; +} + + + + + diff --git a/karavan-app/src/main/webui/src/ui/compass/AppCompass.tsx b/karavan-app/src/main/webui/src/ui/compass/AppCompass.tsx new file mode 100644 index 00000000..f9747da7 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppCompass.tsx @@ -0,0 +1,67 @@ +import React, {lazy, Suspense, useEffect, useMemo} from 'react'; +import {Compass, Drawer, DrawerContent, DrawerContentBody} from '@patternfly/react-core'; +import {AppDock} from "./AppDock"; +import "./AppCompass.css" +import {useCompassStore} from "./useCompassStore"; +import {shallow} from "zustand/shallow"; +import {AppMain} from "@compass/AppMain"; +import {ErrorBoundaryWrapper} from "@shared/ui/ErrorBoundaryWrapper"; +import {useProjectPageStore} from "@page-project/ProjectPageStore"; +import {useDashboardStore} from "@stores/DashboardStore"; +import {useFilesStore} from "@stores/ProjectStore"; +import {useCommandPaletteStore} from "@command-palette/useCommandPaletteStore"; + +// The palette embeds a Monaco editor, so it is only fetched when actually opened. +const CommandPaletteModal = lazy(() => import("@command-palette/CommandPaletteModal").then(m => ({default: m.CommandPaletteModal}))); + +const AppCompass: React.FunctionComponent = () => { + + const [ + isDockExpanded, + isDockTextExpanded, + isDrawerExpanded, + drawerPanelContent, + setIsDrawerExpanded + ] = useCompassStore((s) => [ + s.isDockExpanded, + s.isDockTextExpanded, + s.isDrawerExpanded, + s.drawerPanel, + s.setIsDrawerExpanded + ], shallow); + + const showPalette = useCommandPaletteStore((s) => s.showPalette) + + const showDashboardSideBar = useDashboardStore(s => s.showSideBar, shallow); + const showProjectSideBar = useProjectPageStore(s => s.showSideBar, shallow); + const showFilesSidebar = useFilesStore(s => s.showSideBar, shallow); + + useEffect(() => setIsDrawerExpanded(showDashboardSideBar !== null), [showDashboardSideBar]); + useEffect(() => setIsDrawerExpanded(showProjectSideBar !== null), [showProjectSideBar]); + useEffect(() => setIsDrawerExpanded(showFilesSidebar !== null), [showFilesSidebar]); + + const memoizedDock = useMemo(() => <AppDock/>, []); + const memoizedMain = useMemo(() => <AppMain/>, []); + + return ( + <Drawer isExpanded={isDrawerExpanded} position="end" isPill onExpand={_ => {}}> + <DrawerContent panelContent={drawerPanelContent}> + <DrawerContentBody> + <ErrorBoundaryWrapper onError={error => console.error(error)}> + <> + <Compass + className={"karavan"} + dock={memoizedDock} + isDockExpanded={isDockExpanded} + isDockTextExpanded={isDockTextExpanded} + main={memoizedMain} + /> + {showPalette && <Suspense fallback={null}><CommandPaletteModal/></Suspense>} + </> + </ErrorBoundaryWrapper> + </DrawerContentBody> + </DrawerContent> + </Drawer> + ); +}; +export default AppCompass \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/AppDock.css b/karavan-app/src/main/webui/src/ui/compass/AppDock.css new file mode 100644 index 00000000..b357628c --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppDock.css @@ -0,0 +1,44 @@ +.karavan .pf-v6-c-compass__dock .pf-v6-c-masthead { + row-gap: 8px; +} + +.karavan .pf-v6-c-compass__dock .light-theme-dock { + background-color: var(--pf-t--color--gray--95); + .pf-v6-c-nav__link-text { + color: var(--pf-t--color--gray--10); + } + .pf-m-hamburger svg { + color: var(--pf-t--color--gray--10); + } +} + +.pf-v6-c-compass__dock.pf-m-text-expanded { + width: 10em; +} +.pf-v6-c-masthead.pf-m-docked .pf-v6-c-masthead__main { + gap: var(--pf-t--global--spacer--xs); +} + +.pf-v6-c-masthead__brand { + min-width: 0; +} + +.pf-v6-c-compass__dock { + position: relative; + inset: initial; + width: auto; + translate: 0; +} + +.nav-button-badge { + position: absolute; + top: 6px; + right: 2px; + margin: 0; + padding: 0 5px; + min-width: fit-content; + background-color: var(--pf-t--color--blue--30); + color: var(--pf-t--color--gray--95); + font-weight: normal; + font-size: 9px; +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/AppDock.tsx b/karavan-app/src/main/webui/src/ui/compass/AppDock.tsx new file mode 100644 index 00000000..8eae938a --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppDock.tsx @@ -0,0 +1,192 @@ +import React, {useContext, useRef} from 'react'; +import {useLocation, useNavigate} from 'react-router-dom'; +import { + Badge, + Brand, + Button, + CompassDockMain, + Divider, + Masthead, + MastheadBrand, + MastheadContent, + MastheadLogo, + MastheadMain, + MastheadToggle, + Nav, + NavItem, + NavList, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + Tooltip +} from '@patternfly/react-core'; +import logo from '../shared/icons/logo.svg'; +import {AuthContext} from "@api/auth/AuthProvider"; +import {getNavigationFirstMenu, getNavigationSecondMenu, MenuItem} from "@compass/navigation/NavigationMenu"; +import {useAppConfigStore, useDevModeStore, useFileStore} from "@stores/ProjectStore"; +import {shallow} from "zustand/shallow"; +import {BUILD_IN_PROJECTS} from "@models/ProjectModels"; +import {useUIStore} from "@stores/useUIStore"; +import {useCompassStore} from "./useCompassStore"; +import "./AppDock.css" +import {useTheme} from "@compass/theme/ThemeContext"; +import {useAppConfig} from "@compass/useConfig"; + +interface NavOnSelectProps { + groupId: number | string; + itemId: number | string; + to: string; +} + +export const AppDock: React.FunctionComponent = () => { + + const config = useAppConfigStore((s) => s.config); + const {isDark} = useTheme(); + const {pageId, setPageId} = useUIStore(); + const [setFile] = useFileStore((state) => [state.setFile], shallow) + const [setStatus, setPodName] = useDevModeStore((state) => [state.setStatus, state.setPodName], shallow) + const {isDockExpanded, isDockTextExpanded, setIsDockTextExpanded} = useCompassStore(); + const {isDev} = useAppConfig(); + const navigate = useNavigate(); + const location = useLocation(); + const {logout} = useContext(AuthContext); + const firstMenu = getNavigationFirstMenu() + const secondMenu = getNavigationSecondMenu(config.environment, config.infrastructure); + + React.useEffect(() => { + const page = location.pathname?.split("/").filter(Boolean)[0]; + if (page === 'projects') { + const projectId = location.pathname?.split("/").filter(Boolean)[1]; + if (BUILD_IN_PROJECTS.includes(projectId)) { + setPageId('settings'); + } else { + setPageId(page); + } + } else if (page !== undefined) { + setPageId(page); + } else if (config.environment === 'dev') { + setPageId('projects'); + } else { + setPageId('dashboard'); + } + }, [location]); + + + // Intercept PatternFly Nav selections and route via React Router + const onNavSelect = (_event: React.FormEvent<HTMLInputElement>, selectedItem: NavOnSelectProps) => { + if (selectedItem.to) { + navigate(selectedItem.to); + } + }; + + function onClick(page: MenuItem) { + if (page.pageId === 'logout') { + logout(); + } else { + setFile('none', undefined); + setPodName(undefined); + setStatus("none"); + setPageId(page.pageId); + navigate(page.pageId); + } + } + + const dockedToggleRef = useRef<HTMLButtonElement>(null); + + const onToggleDock = () => { + setIsDockTextExpanded(!isDockTextExpanded); + }; + + function getMenu(menu: MenuItem[]) { + return ( + menu.filter(menuItem => isDev || (!menuItem.hideInNonDev && !isDev)) + .map((menuItem, index) => { + const isSelected = pageId === menuItem.pageId; + const notExpanded = !isDockTextExpanded && !isDockExpanded; + const navItem = + <NavItem + key={menuItem.pageId} + preventDefault + id={menuItem.pageId} + itemId={menuItem.pageId} + isActive={isSelected} + icon={menuItem.icon} + aria-label={menuItem.name} + onClick={() => onClick(menuItem)} + > + {isDockTextExpanded && menuItem.name} + </NavItem> + if (notExpanded) { + return <Tooltip key={menuItem.pageId} aria="none" aria-live="off" content={menuItem.name}> + {navItem} + </Tooltip> + } else { + return ( + <div style={{position: "relative"}}> + {navItem} + {menuItem.preview && <Badge className='nav-button-badge'>Preview</Badge>} + </div> + ) + } + }) + ) + } + + return ( + <CompassDockMain> + <Masthead display={{ default: 'inline' }} id="docked-masthead" variant="docked" className={isDark ? "" : "light-theme-dock"}> + <MastheadMain style={{display: 'flex', flexDirection: isDockTextExpanded ? 'row' : 'column'}}> + <MastheadToggle> + <Button + ref={dockedToggleRef} + variant="plain" + isHamburger + onClick={onToggleDock} + aria-label="Global navigation" + isExpanded={isDockTextExpanded} + /> + </MastheadToggle> + <MastheadBrand> + <MastheadLogo isCompact> + </MastheadLogo> + <MastheadLogo> + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-around', + width: !isDockTextExpanded ? '100%' : '5em' + }}> + <Brand src={logo} alt="Karavan" heights={{default: '37px'}}/> + </div> + </MastheadLogo> + </MastheadBrand> + </MastheadMain> + <Divider /> + <MastheadContent> + <Toolbar id="toolbar" isVertical> + <ToolbarContent> + <ToolbarItem> + <Nav variant="docked" aria-label="First" ouiaId="IconNavFirst"> + <NavList> + {getMenu(firstMenu)} + </NavList> + </Nav> + </ToolbarItem> + <ToolbarGroup + variant="action-group-plain" + align={{ default: 'alignEnd' }} + gap={{ default: 'gapNone', md: 'gapMd' }} + > + <ToolbarItem> + <Nav variant="docked" aria-label="Second" ouiaId="IconNavSecond"> + <NavList> + {getMenu(secondMenu)} + </NavList> + </Nav> + </ToolbarItem> + </ToolbarGroup> + </ToolbarContent> + </Toolbar> + </MastheadContent> + </Masthead> + </CompassDockMain> + ); +}; \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/AppFooter.css b/karavan-app/src/main/webui/src/ui/compass/AppFooter.css new file mode 100644 index 00000000..c426e6d9 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppFooter.css @@ -0,0 +1,4 @@ +.prod-environment { + background-color: var(--pf-t--global--icon--color--status--danger--default); + color: var(--pf-t--global--background--color--secondary--default); +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/AppFooter.tsx b/karavan-app/src/main/webui/src/ui/compass/AppFooter.tsx new file mode 100644 index 00000000..e91d6228 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppFooter.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import {Badge, CompassMainFooter, Divider, Panel, PanelMain, PanelMainBody} from '@patternfly/react-core'; +import {useProjectStore} from "@stores/ProjectStore"; +import "./AppFooter.css" +import {PlatformVersions} from "@shared/ui/PlatformLogos"; +import {BUILD_IN_PROJECTS} from "@models/ProjectModels"; +import {ProjectStatusLabel} from "@page-projects/table/ProjectStatusLabel"; +import {useAppConfig} from "@compass/useConfig"; + +export const AppFooter: React.FunctionComponent = () => { + + const {environment, infrastructure} = useAppConfig() + const project = useProjectStore((s) => s.project) + const isBuildIn = BUILD_IN_PROJECTS.includes(project?.projectId); + const {isDev} = useAppConfig(); + const envClassName = isDev ? "" : "prod-environment" + + const environmentUI = + <div style={{display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', gap: 6, alignItems:'center'}}> + <Badge isRead className={envClassName}>{environment}</Badge> + <Badge isRead className={envClassName}>{infrastructure}</Badge> + </div> + + return ( + <CompassMainFooter className={"app-footer"}> + <Panel isGlass style={{width: '100%', padding: 8}}> + <PanelMain> + <PanelMainBody> + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8}}> + {/*{PlatformName(12, 175)}*/} + <PlatformVersions/> + <Divider orientation={{default: 'vertical'}}/> + {environmentUI} + <Divider orientation={{default: 'vertical'}}/> + {!isBuildIn && <ProjectStatusLabel projectId={project?.projectId}/>} + <div style={{flex: 1}}/> + </div> + </PanelMainBody> + </PanelMain> + </Panel> + </CompassMainFooter> + ); +}; \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/AppMain.css b/karavan-app/src/main/webui/src/ui/compass/AppMain.css new file mode 100644 index 00000000..054fb28d --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppMain.css @@ -0,0 +1,6 @@ +.pf-v6-c-panel__main-body { + padding-block-start: 0; + padding-block-end: 0; + padding-inline-start: 0; + padding-inline-end: 0; +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/AppMain.tsx b/karavan-app/src/main/webui/src/ui/compass/AppMain.tsx new file mode 100644 index 00000000..51845316 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppMain.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import {CompassContent, CompassHeader, Panel, PanelMain, PanelMainBody} from '@patternfly/react-core'; + +import "./AppMain.css" +import {MainRoutes} from "@compass/navigation/MainRoutes"; +import {AppNavigation} from "./AppNavigation"; +import {AppFooter} from "@compass/AppFooter"; + +export const AppMain: React.FunctionComponent = () => { + + return ( + <> + <CompassHeader logo={<AppNavigation/>}/> + <CompassContent> + <Panel isScrollable isAutoHeight isGlass style={{overflow: "hidden", border: "1px solid var(--pf-t--global--background--color--primary--default)"}}> + <PanelMain style={{height:'100%'}}> + <PanelMainBody style={{height:'100%'}}> + <MainRoutes/> + </PanelMainBody> + </PanelMain> + </Panel> + </CompassContent> + <AppFooter/> + </> + ); +}; \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/AppNavigation.tsx b/karavan-app/src/main/webui/src/ui/compass/AppNavigation.tsx new file mode 100644 index 00000000..a4262aee --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/AppNavigation.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import {CompassNavContent, CompassNavMain, Divider, Panel, PanelMain, PanelMainBody} from '@patternfly/react-core'; +import {useCompassStore} from "./useCompassStore"; +import {shallow} from "zustand/shallow"; +import DarkModeToggle from "./theme/DarkModeToggle"; + +export const AppNavigation: React.FunctionComponent = () => { + + const [pageNav, pageTools] = useCompassStore((s) => [ + s.pageNav, + s.pageTools + ], shallow); + + return ( + <Panel isGlass> + <PanelMain> + <PanelMainBody> + <CompassNavContent style={{justifyContent: 'space-between'}}> + <CompassNavMain> + {pageNav} + </CompassNavMain> + <CompassNavMain> + {pageTools && ( + <div style={{ display: 'flex', alignItems: 'center', paddingBottom: 6, paddingTop: 6, gap: 6 }}> + {pageTools} + <Divider orientation={{default: 'vertical'}}/> + <DarkModeToggle/> + </div> + )} + </CompassNavMain> + </CompassNavContent> + </PanelMainBody> + </PanelMain> + </Panel> + ); +}; \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/ReadinessPanel.tsx b/karavan-app/src/main/webui/src/ui/compass/ReadinessPanel.tsx new file mode 100644 index 00000000..6e80972f --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/ReadinessPanel.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import '@compass/App.css'; +import {useReadinessStore} from "@stores/ReadinessStore"; +import {useDataPolling} from "@shared/polling/useDataPolling"; +import {Bullseye, Content, ContentVariants, Flex, FlexItem, ProgressStep, ProgressStepper, Spinner, Tooltip, TooltipPosition} from "@patternfly/react-core"; +import {useMainHook} from "@compass/useMainHook"; +import {PlatformLogoBase64} from "@compass/navigation/PlatformLogo"; + +const FAST_INTERVAL = 1000; // 1 second (when not ready) +const SLOW_INTERVAL = 10000; // 10 seconds (when ready) + +export function ReadinessPanel() { + + const { readiness, fetchReadiness } = useReadinessStore(); + const isReady = readiness && readiness.status === true; + const currentInterval = isReady ? SLOW_INTERVAL : FAST_INTERVAL; + useDataPolling('readiness', fetchReadiness, currentInterval); + const {showSpinner, showStepper} = useMainHook(); + + function getStepper() { + const steps: any[] = Array.isArray(readiness?.checks) ? readiness.checks : []; + return ( + <Bullseye className="loading-page"> + <Flex direction={{default: "column"}} justifyContent={{default: "justifyContentCenter"}}> + <FlexItem style={{textAlign: "center"}}> + <img src={PlatformLogoBase64()} className="logo" alt='logo'/> + <Content> + <Content component={ContentVariants.h2}> + Waiting for services + </Content> + </Content> + </FlexItem> + <FlexItem> + <ProgressStepper aria-label="Readiness progress" isCenterAligned isVertical> + {steps.map(step => ( + <ProgressStep + key={step.name} + variant={step.status === 'UP' ? "success" : "info"} + isCurrent={step.status !== 'UP'} + icon={step.status !== 'UP' ? <Spinner isInline aria-label="Loading..."/> : undefined} + id={step.name} + titleId={step.name} + aria-label={step.name} + > + {step.name} + </ProgressStep> + ))} + </ProgressStepper> + </FlexItem> + </Flex> + </Bullseye> + ) + } + + function getSpinner() { + return ( + <Bullseye className="loading-page"> + <Spinner className="spinner" diameter="140px" aria-label="Loading..."/> + <Tooltip content="Connecting to server..." position={TooltipPosition.bottom}> + <div className="logo-placeholder"> + <img src={PlatformLogoBase64()} className="logo" alt='logo'/> + </div> + </Tooltip> + </Bullseye> + ) + } + + return ( + <> + {showSpinner() && getSpinner()} + {showStepper() && getStepper()} + </> + ) +} diff --git a/karavan-app/src/main/webui/src/ui/compass/navigation/MainRoutes.tsx b/karavan-app/src/main/webui/src/ui/compass/navigation/MainRoutes.tsx new file mode 100644 index 00000000..12276601 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/navigation/MainRoutes.tsx @@ -0,0 +1,89 @@ +/* + * 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 {Navigate, Route, Routes} from 'react-router-dom'; +import React, {lazy, Suspense} from "react"; +import {NotAuthorizedPage} from "@compass/navigation/NotAuthorizedPage"; +import {ROUTES} from "./Routes"; +import {ProtectedRoute} from "@compass/navigation/ProtectedRoute"; +import {PageFallback} from "@compass/navigation/PageFallback"; +import {LoginPage} from "@login/LoginPage"; + +const ProjectsPage = lazy(() => import("@page-projects/ProjectsPage").then(m => ({default: m.ProjectsPage}))); +const ProjectPage = lazy(() => import("@page-project/ProjectPage").then(m => ({default: m.ProjectPage}))); +const SettingsPage = lazy(() => import("@page-settings/SettingsPage").then(m => ({default: m.SettingsPage}))); +const SystemPage = lazy(() => import("@page-system/SystemPage").then(m => ({default: m.SystemPage}))); +const DocumentationPage = lazy(() => import("@page-documentation/DocumentationPage").then(m => ({default: m.DocumentationPage}))); +const AccessPage = lazy(() => import("@page-access/AccessPage").then(m => ({default: m.AccessPage}))); + +export function MainRoutes() { + + return ( + <Suspense fallback={<PageFallback/>}> + <Routes> + <Route path={ROUTES.LOGIN} element={ + <ProtectedRoute> + <LoginPage/> + </ProtectedRoute>} + /> + <Route path={ROUTES.PROJECTS} element={ + <ProtectedRoute> + <ProjectsPage key="integrations"/> + </ProtectedRoute> + }/> + <Route path={ROUTES.PROJECT_DETAIL} element={ + <ProtectedRoute> + <ProjectPage key="project"/> + </ProtectedRoute> + }/> + <Route path={ROUTES.PROJECT_FILE} element={ + <ProtectedRoute> + <ProjectPage key="project"/> + </ProtectedRoute> + }/> + <Route path={ROUTES.SETTINGS} element={ + <ProtectedRoute> + <SettingsPage/> + </ProtectedRoute> + }/> + <Route path={ROUTES.SETTINGS_FILE} element={ + <ProtectedRoute> + <SettingsPage/> + </ProtectedRoute> + }/> + <Route path={ROUTES.SYSTEM} element={ + <ProtectedRoute> + <SystemPage/> + </ProtectedRoute> + }/> + <Route path={ROUTES.DOCUMENTATION} element={ + <ProtectedRoute> + <DocumentationPage/> + </ProtectedRoute> + }/> + <Route path={ROUTES.ACL} element={ + <ProtectedRoute> + <AccessPage/> + </ProtectedRoute> + }/> + <Route path={ROUTES.FORBIDDEN} element={ + <NotAuthorizedPage/> + }/> + <Route path="*" element={<Navigate to={ROUTES.PROJECTS} replace/>}/> + </Routes> + </Suspense> + ) +} diff --git a/karavan-app/src/main/webui/src/ui/compass/navigation/NavigationMenu.tsx b/karavan-app/src/main/webui/src/ui/compass/navigation/NavigationMenu.tsx new file mode 100644 index 00000000..52b9466f --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/navigation/NavigationMenu.tsx @@ -0,0 +1,52 @@ +import {AuthApi, getCurrentUser} from "@api/auth/AuthApi"; +import React from "react"; +import {SvgNavigationIcon} from "@shared/icons/SvgNavigationIcon"; +import {DockerIcon} from "@patternfly/react-icons"; +import {LogoKubernetes} from "@carbon/icons-react"; + +export class MenuItem { + pageId: string = ''; + name: string = ''; + icon: any; + preview: boolean = false; + hideInNonDev: boolean = false; + + constructor(pageId: string, name: string, icon: any, preview: boolean = false, hideInNonDev: boolean = false) { + this.pageId = pageId; + this.name = name; + this.icon = icon; + this.preview = preview; + this.hideInNonDev = hideInNonDev; + } +} + +export function getNavigationFirstMenu(): MenuItem[] { + return [ + new MenuItem("projects", "Projects", SvgNavigationIcon({icon: 'apps'})), + new MenuItem("settings", "Settings", SvgNavigationIcon({icon: 'settings'}), false, true) + ]; +} + + +export function getNavigationSecondMenu(environment: string, infrastructure: string): MenuItem[] { + const iconInfra = infrastructure === 'kubernetes' ? <LogoKubernetes className={"infra-icon-k8s"}/> : <DockerIcon className='infra-icon-docker'/>; + + const pages: MenuItem[] = [] + + if (environment === 'dev') { + pages.push(new MenuItem("documentation", "Learn", SvgNavigationIcon({icon: 'documentation'}))); + } + + if (getCurrentUser()?.roles?.includes('platform-admin')) { + pages.push(new MenuItem("system", "System", iconInfra)); + } + + if (AuthApi.authType === 'session') { + pages.push(new MenuItem("acl", "Access", SvgNavigationIcon({icon: 'access'}))); + } + + pages.push(new MenuItem("logout", "Logout", SvgNavigationIcon({icon: 'logout'}))); + + return pages; +} + diff --git a/karavan-app/src/main/webui/src/ui/compass/navigation/NotAuthorizedPage.tsx b/karavan-app/src/main/webui/src/ui/compass/navigation/NotAuthorizedPage.tsx new file mode 100644 index 00000000..d6f4bb0a --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/navigation/NotAuthorizedPage.tsx @@ -0,0 +1,26 @@ +import React, {useEffect} from "react"; +import {Bullseye, EmptyState, EmptyStateBody, EmptyStateVariant} from "@patternfly/react-core"; +import {UserSecretIcon} from "@patternfly/react-icons"; +import {useNavigate} from "react-router-dom"; +import {getCurrentUser} from "@api/auth/AuthApi"; + +export function NotAuthorizedPage() { + + const navigate = useNavigate(); + + useEffect(() => { + const roles = getCurrentUser()?.roles ?? []; + const authorized = roles?.length > 0; + if (authorized) { + navigate("/"); + } + }, []); + + return ( + <Bullseye> + <EmptyState headingLevel="h4" icon={UserSecretIcon} titleText="Not authorized" variant={EmptyStateVariant.xl}> + <EmptyStateBody>You are not authorize to use this application</EmptyStateBody> + </EmptyState> + </Bullseye> + ) +} diff --git a/karavan-app/src/main/webui/src/ui/compass/navigation/PageFallback.tsx b/karavan-app/src/main/webui/src/ui/compass/navigation/PageFallback.tsx new file mode 100644 index 00000000..1a44c0ea --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/navigation/PageFallback.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import {Bullseye, Spinner} from "@patternfly/react-core"; + +/** + * Shown while a lazily loaded page chunk is being fetched. + */ +export function PageFallback() { + return ( + <Bullseye> + <Spinner aria-label="Loading page"/> + </Bullseye> + ); +} diff --git a/karavan-app/src/main/webui/src/ui/compass/navigation/PlatformLogo.tsx b/karavan-app/src/main/webui/src/ui/compass/navigation/PlatformLogo.tsx new file mode 100644 index 00000000..a59ff3a1 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/navigation/PlatformLogo.tsx @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react"; +import {KaravanIcon} from "@designer/icons/KaravanIcons"; + +export function PlatformLogoBase64(customLogo?: string) { + + return customLogo + ? customLogo + : 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgICAgICB3aWR0aD0iMzYwLjEzODg1IgogICAgICAgIGhlaWdodD0iMzYwLjEzODg1IgogICAgICAgIHZpZXdCb3g9IjAgMCAzNjAuMTM4ODUgMzYwLjEzODg1IgogICAgICAgIHZlcnNpb249IjEuMSIKICAgICAgICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIKICAgICAgICBpZD0ic3ZnNTAiCiAgICAgICAgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiCiAgICAgICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcwogICA [...] +} + +export function PlatformLogo(classNames?: string) { + return KaravanIcon(classNames) +} + +export default PlatformLogo; diff --git a/karavan-app/src/main/webui/src/ui/compass/navigation/ProtectedRoute.tsx b/karavan-app/src/main/webui/src/ui/compass/navigation/ProtectedRoute.tsx new file mode 100644 index 00000000..2a42d593 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/navigation/ProtectedRoute.tsx @@ -0,0 +1,32 @@ +import {Navigate, useLocation} from "react-router-dom"; +import {JSX, useContext} from "react"; +import {AuthContext} from "@api/auth/AuthProvider"; +import {ROUTES} from "@compass/navigation/Routes"; +import {useReadinessStore} from "@stores/ReadinessStore"; +import {PageFallback} from "@compass/navigation/PageFallback"; + +export function ProtectedRoute({ children }: { children: JSX.Element }) { + const { readiness } = useReadinessStore(); + const { user, loading, authType } = useContext(AuthContext); + const location = useLocation(); + + if (readiness === undefined || readiness.status !== true) { + return children; // stay on loader page if already there + } + + // OIDC never uses the internal login page: the browser is redirected to + // Keycloak by SsoApi, so show a spinner while that is in flight. + if (!user && authType === 'oidc') { + return <PageFallback/>; + } + + if (!user && location.pathname !== ROUTES.LOGIN) { + return <Navigate to={ROUTES.LOGIN} state={{ from: location }} replace />; + } + + if (user && location.pathname === ROUTES.LOGIN) { + return <Navigate to={ROUTES.ROOT} state={{ from: location }} />; + } + + return children; +} diff --git a/karavan-app/src/main/webui/src/ui/compass/navigation/Routes.ts b/karavan-app/src/main/webui/src/ui/compass/navigation/Routes.ts new file mode 100644 index 00000000..3657c19a --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/navigation/Routes.ts @@ -0,0 +1,15 @@ +// routes.ts +export const ROUTES = { + PROJECTS: "/projects", + PROJECT_DETAIL: "/projects/:projectId", + PROJECT_FILE: "/projects/:projectId/:fileName", + SYSTEM: "/system", + SETTINGS: "/settings", + SETTINGS_FILE: "/settings/:projectId/:fileName", + DOCUMENTATION: "/documentation", + FORBIDDEN: "/403", + ACL: "/acl", + LOGIN: "/login", + // fallback redirect + ROOT: "/", +}; diff --git a/karavan-app/src/main/webui/src/ui/compass/theme/DarkModeToggle.tsx b/karavan-app/src/main/webui/src/ui/compass/theme/DarkModeToggle.tsx new file mode 100644 index 00000000..d8aa9ec3 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/theme/DarkModeToggle.tsx @@ -0,0 +1,28 @@ +import {ToggleGroup, ToggleGroupItem} from '@patternfly/react-core'; +import {useTheme} from './ThemeContext'; +import {Moon, Sun} from "@carbon/icons-react"; + +const DarkModeToggle = () => { + const { isDark, toggleDarkMode } = useTheme(); + + return ( + <ToggleGroup aria-label="DarkModeToggle" className={"dark-mode-toggle"} isCompact> + <ToggleGroupItem + icon={<Sun className={"carbon"}/>} + aria-label="light" + buttonId="toggle-group-icons-1" + isSelected={!isDark} + onChange={(_, selected) => toggleDarkMode(!selected)} + /> + <ToggleGroupItem + icon={<Moon className={"carbon"}/>} + aria-label="dark" + buttonId="toggle-group-icons-2" + isSelected={isDark} + onChange={(_, selected) => toggleDarkMode(selected)} + /> + </ToggleGroup> + ); +}; + +export default DarkModeToggle; diff --git a/karavan-app/src/main/webui/src/ui/compass/theme/ThemeContext.tsx b/karavan-app/src/main/webui/src/ui/compass/theme/ThemeContext.tsx new file mode 100644 index 00000000..965a87f0 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/theme/ThemeContext.tsx @@ -0,0 +1,50 @@ +import React, {createContext, useContext, useEffect, useState} from 'react'; + +interface ThemeContextType { + isDark: boolean; + toggleDarkMode: (checked: boolean) => void; +} + +const ThemeContext = createContext<ThemeContextType | undefined>(undefined); + +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isDark, setIsDark] = useState(false); + + useEffect(() => { + const storedTheme = localStorage.getItem('pf-theme'); + + let shouldUseDark = false; + + if (storedTheme === 'dark') { + shouldUseDark = true; + } else if (storedTheme === 'light') { + shouldUseDark = false; + } else { + // No stored preference → use browser preference + shouldUseDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + } + + setIsDark(shouldUseDark); + document.documentElement.classList.toggle('pf-v6-theme-dark', shouldUseDark); + }, []); + + const toggleDarkMode = (checked: boolean) => { + setIsDark(checked); + document.documentElement.classList.toggle('pf-v6-theme-dark', checked); + localStorage.setItem('pf-theme', checked ? 'dark' : 'light'); + }; + + return ( + <ThemeContext.Provider value={{ isDark, toggleDarkMode }}> + {children} + </ThemeContext.Provider> + ); +}; + +export const useTheme = (): ThemeContextType => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; diff --git a/karavan-app/src/main/webui/src/ui/compass/useCompassStore.ts b/karavan-app/src/main/webui/src/ui/compass/useCompassStore.ts new file mode 100644 index 00000000..ed9cc2b5 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/useCompassStore.ts @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from "react"; +import {shallow} from "zustand/shallow"; +import {createWithEqualityFn} from "zustand/traditional"; + +interface CompassState { + isDockExpanded: boolean; + isDockTextExpanded: boolean; + isDrawerExpanded: boolean; + + // Add state to hold the injected panel headers + pageTitle: React.ReactNode; + pageNav: React.ReactNode; + pageTools: React.ReactNode; + drawerPanel: React.ReactNode; + + setIsDockExpanded: (isDockExpanded: boolean) => void; + setIsDockTextExpanded: (isDockTextExpanded: boolean) => void; + setIsDrawerExpanded: (isDrawerExpanded: boolean) => void; + + // Action to set the context from RightPanel + setPageContext: (title: React.ReactNode, nav: React.ReactNode, tools: React.ReactNode, drawerPanel: React.ReactNode) => void; + setDrawerPanel: (drawerPanel: React.ReactNode) => void; +} + +export const useCompassStore = createWithEqualityFn<CompassState>((set, get) => ({ + // Initial State + isDockExpanded: false, + isDockTextExpanded: false, + isDrawerExpanded: false, + + pageTitle: null, + pageNav: null, + pageTools: null, + drawerPanel: null, + + // Actions + setIsDockExpanded: (isDockExpanded) => set({ isDockExpanded }), + setIsDockTextExpanded: (isDockTextExpanded) => set({ isDockTextExpanded }), + setIsDrawerExpanded: (isDrawerExpanded) => set({ isDrawerExpanded }), + + setPageContext: (pageTitle, pageNav, pageTools, drawerPanel) => set({ pageTitle, pageNav, pageTools, drawerPanel }), + setDrawerPanel: (drawerPanel) => set({ drawerPanel }), +}), shallow); \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/compass/useConfig.tsx b/karavan-app/src/main/webui/src/ui/compass/useConfig.tsx new file mode 100644 index 00000000..eccd7425 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/useConfig.tsx @@ -0,0 +1,29 @@ +/* + * 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 {useAppConfigStore} from "@stores/ProjectStore"; + +export const useAppConfig = () => { + const { config } = useAppConfigStore(); + + return { + isDev: config?.environment === 'dev', + environment: config?.environment, + environments: config?.environments, + infrastructure: config?.infrastructure, + isKubernetes: config?.infrastructure === 'kubernetes', + }; +}; diff --git a/karavan-app/src/main/webui/src/ui/compass/useMainHook.tsx b/karavan-app/src/main/webui/src/ui/compass/useMainHook.tsx new file mode 100644 index 00000000..249c377a --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/compass/useMainHook.tsx @@ -0,0 +1,153 @@ +/* + * 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 {ComponentApi} from "@core/api/ComponentApi"; +import {AppConfig, Project} from "@models/ProjectModels"; +import {useAppConfigStore, useProjectsStore} from "@stores/ProjectStore"; +import {InfrastructureAPI} from "@designer/utils/InfrastructureAPI"; +import {shallow} from "zustand/shallow"; +import {ProjectService} from "@services/ProjectService"; +import {SpiBeanApi} from "@core/api/SpiBeanApi"; +import {MainConfigurationApi} from "@core/api/MainConfigurationApi"; +import {useContext} from "react"; +import {AuthContext} from "@api/auth/AuthProvider"; +import {useReadinessStore} from "@stores/ReadinessStore"; +import {AuthApi} from "@api/auth/AuthApi"; +import {useContainerStatusesStore} from "@stores/ContainerStatusesStore"; +import {useTemplatesStore} from "@stores/SettingsStore"; + +export function useMainHook () { + + const {readiness} = useReadinessStore(); + const [setConfig, setDockerInfo] = useAppConfigStore((s) => [s.setConfig, s.setDockerInfo], shallow) + const [setProjects] = useProjectsStore((s) => [s.setProjects], shallow) + const {fetchContainers} = useContainerStatusesStore(); + const fetchTemplateFiles = useTemplatesStore((s) => s.fetchTemplateFiles); + const [selectedEnv, selectEnvironment] = useAppConfigStore((state) => [state.selectedEnv, state.selectEnvironment], shallow) + const { user } = useContext(AuthContext); + + const getStatuses = () => { + if (user) { + fetchContainers(); + } + } + + const getData = () => { + if (user) { + KaravanApi.getConfiguration((config: AppConfig) => { + setConfig(config); + if (!selectedEnv || selectedEnv.length == 0) { + selectEnvironment(config.environment, true); + } + if (config.infrastructure !== 'kubernetes') { + KaravanApi.getInfrastructureInfo((info: any) => { + setDockerInfo(info) + }) + } + InfrastructureAPI.infrastructure = config.infrastructure; + }); + KaravanApi.getProjects((projects: Project[]) => { + setProjects(projects); + }); + updateComponents(); + updateBeans(); + updateAllConfigurations(); + fetchTemplateFiles(); + ProjectService.loadCustomKamelets(); + ProjectService.loadBlockedComponentAndKamelets(); + } + } + + async function updateComponents(): Promise<void> { + await new Promise(resolve => { + KaravanApi.getComponents(code => { + const components: [] = JSON.parse(code); + const jsons: string[] = []; + components.forEach(c => jsons.push(JSON.stringify(c))); + ComponentApi.saveComponents(jsons, true); + }) + }); + } + + async function updateBeans(): Promise<void> { + await new Promise(resolve => { + KaravanApi.getBeans(code => { + const beans: [] = JSON.parse(code); + const jsons: string[] = []; + beans.forEach(c => jsons.push(JSON.stringify(c))); + SpiBeanApi.saveSpiBeans(jsons, true); + }) + }); + } + + async function updateConfiguration(configName: string): Promise<{ properties: any[]; groups: any[] }> { + return new Promise(resolve => { + KaravanApi.getMetadataConfiguration(configName, code => { + try { + const objects: any = JSON.parse(code); + const properties: any[] = objects.properties || []; + const groups: any[] = objects.groups || []; + resolve({ properties, groups }); + } catch (error) { + console.error(`Failed to parse configuration for "${configName}":`, error); + resolve({ properties: [], groups: [] }); + } + }); + }); + } + + async function updateConfigurationChanges(): Promise<any[]> { + return new Promise(resolve => { + KaravanApi.getConfigurationChanges(code => { + const changes: any = JSON.parse(code); + resolve(changes); + }); + }); + } + + async function updateAllConfigurations(): Promise<void> { + const [meta, jbang, jib, jkube, changes] = await Promise.all([ + updateConfiguration("main"), + updateConfiguration("jbang"), + updateConfiguration("jib"), + updateConfiguration("jkube"), + updateConfigurationChanges() + ]); + + const properties: any[] = [...meta.properties, ...jbang.properties, ...jib.properties, ...jkube.properties] + const groups: any[] = [...meta.groups, ...jbang.groups, ...jib.groups, ...jkube.groups] + + MainConfigurationApi.saveApplicationProperties(properties, true); + MainConfigurationApi.saveApplicationPropertyGroups(groups); + MainConfigurationApi.saveApplicationPropertyChanges(changes); + } + + function showSpinner() { + return readiness === undefined; + } + + function showStepper() { + return readiness !== undefined && readiness.status !== true; + } + + function showApplication() { + return AuthApi.authType !== undefined && readiness?.status === true; + } + + + return { getData, getStatuses, showSpinner, showStepper, showApplication }; +} \ No newline at end of file
