This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
commit 09ea30ee09b10db001105bdcfe7f901a4592690a Author: Marat Gubaidullin <[email protected]> AuthorDate: Fri Feb 27 16:43:56 2026 -0500 Front-end App for 4.18.0 --- karavan-app/src/main/webui/src/karavan/app/App.tsx | 20 +- .../src/main/webui/src/karavan/app/MainHook.tsx | 31 +- .../main/webui/src/karavan/app/ReadinessPanel.tsx | 29 +- .../main/webui/src/karavan/app/login/LoginPage.css | 358 ++++++++++++++++++++- .../main/webui/src/karavan/app/login/LoginPage.tsx | 215 ++++++++++--- .../webui/src/karavan/app/login/UserPopupOidc.tsx | 1 - .../src/karavan/app/navigation/MainRoutes.tsx | 57 +++- .../src/karavan/app/navigation/NavigationMenu.tsx | 50 ++- .../karavan/app/navigation/NotAuthorizedPage.tsx | 16 + .../src/karavan/app/navigation/PageNavigation.css | 178 ++++++---- .../src/karavan/app/navigation/PageNavigation.tsx | 137 ++++---- .../webui/src/karavan/app/navigation/Routes.ts | 27 +- .../webui/src/karavan/app/theme/DarkModeToggle.tsx | 16 + .../webui/src/karavan/app/theme/ThemeContext.tsx | 45 ++- 14 files changed, 940 insertions(+), 240 deletions(-) diff --git a/karavan-app/src/main/webui/src/karavan/app/App.tsx b/karavan-app/src/main/webui/src/karavan/app/App.tsx index e7f7536e..a75ee5d1 100644 --- a/karavan-app/src/main/webui/src/karavan/app/App.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/App.tsx @@ -1,12 +1,28 @@ +/* + * 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 './App.css'; import {mainHook} from "./MainHook"; -import {Notification} from "@features/integration/designer/utils/Notification"; +import {Notification} from "@features/project/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 {PageNavigation} from "@app/navigation/PageNavigation"; +import PageNavigation from "@app/navigation/PageNavigation"; import {MainRoutes} from "@app/navigation/MainRoutes"; import {ReadinessPanel} from "@app/ReadinessPanel"; import {useReadinessStore} from "@stores/ReadinessStore"; diff --git a/karavan-app/src/main/webui/src/karavan/app/MainHook.tsx b/karavan-app/src/main/webui/src/karavan/app/MainHook.tsx index b27c052a..09f53db3 100644 --- a/karavan-app/src/main/webui/src/karavan/app/MainHook.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/MainHook.tsx @@ -1,8 +1,24 @@ +/* + * 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 "@karavan-core/api/ComponentApi"; -import {AppConfig, ContainerStatus, Project} from "@models/ProjectModels"; -import {useAppConfigStore, useProjectsStore, useStatusesStore} from "@stores/ProjectStore"; -import {InfrastructureAPI} from "@features/integration/designer/utils/InfrastructureAPI"; +import {AppConfig, Project} from "@models/ProjectModels"; +import {useAppConfigStore, useProjectsStore} from "@stores/ProjectStore"; +import {InfrastructureAPI} from "@features/project/designer/utils/InfrastructureAPI"; import {shallow} from "zustand/shallow"; import {ProjectService} from "@services/ProjectService"; import {SpiBeanApi} from "@karavan-core/api/SpiBeanApi"; @@ -11,21 +27,20 @@ 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"; export function mainHook () { const {readiness} = useReadinessStore(); const [setConfig, setDockerInfo] = useAppConfigStore((s) => [s.setConfig, s.setDockerInfo], shallow) const [setProjects] = useProjectsStore((s) => [s.setProjects], shallow) - const [setContainers] = useStatusesStore((state) => [state.setContainers], shallow); + const {fetchContainers} = useContainerStatusesStore(); const [selectedEnv, selectEnvironment] = useAppConfigStore((state) => [state.selectedEnv, state.selectEnvironment], shallow) const { user } = useContext(AuthContext); const getStatuses = () => { if (user) { - KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => { - setContainers(statuses); - }); + fetchContainers(); } } @@ -50,8 +65,6 @@ export function mainHook () { updateBeans(); updateAllConfigurations(); ProjectService.loadCamelAndCustomKamelets(); - ProjectService.reloadBlockedTemplates(); - // updateSupportedComponents(); // not implemented yet } } diff --git a/karavan-app/src/main/webui/src/karavan/app/ReadinessPanel.tsx b/karavan-app/src/main/webui/src/karavan/app/ReadinessPanel.tsx index b777151b..9f75b8d0 100644 --- a/karavan-app/src/main/webui/src/karavan/app/ReadinessPanel.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/ReadinessPanel.tsx @@ -1,27 +1,36 @@ +/* + * 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 './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 {mainHook} from "@app/MainHook"; -import {KaravanIcon} from "@features/integration/designer/icons/KaravanIcons"; +import {KaravanIcon} from "@features/project/designer/icons/KaravanIcons"; +import {PlatformLogoBase64} from "@app/navigation/PlatformLogo"; const FAST_INTERVAL = 1000; // 1 second (when not ready) const SLOW_INTERVAL = 10000; // 10 seconds (when ready) export function ReadinessPanel() { - // 1. Subscribe to the state and action const { readiness, fetchReadiness } = useReadinessStore(); - - // 2. Determine the dynamic interval based on the current state const isReady = readiness && readiness.status === true; - const currentInterval = isReady ? SLOW_INTERVAL : FAST_INTERVAL; - - console.log(`Polling interval set to: ${currentInterval}ms`); - - // 3. Pass the dynamic interval to the hook useDataPolling('readiness', fetchReadiness, currentInterval); const {showSpinner, showStepper} = mainHook(); @@ -31,7 +40,7 @@ export function ReadinessPanel() { <Bullseye className="loading-page"> <Flex direction={{default: "column"}} justifyContent={{default: "justifyContentCenter"}}> <FlexItem style={{textAlign: "center"}}> - {KaravanIcon()} + <img src={PlatformLogoBase64()} className="logo" alt='logo'/> <Content> <Content component={ContentVariants.h2}> Waiting for services diff --git a/karavan-app/src/main/webui/src/karavan/app/login/LoginPage.css b/karavan-app/src/main/webui/src/karavan/app/login/LoginPage.css index 439a3243..40e29f1a 100644 --- a/karavan-app/src/main/webui/src/karavan/app/login/LoginPage.css +++ b/karavan-app/src/main/webui/src/karavan/app/login/LoginPage.css @@ -1,19 +1,355 @@ -.karavan .login-page { +.karavan-container { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; + height: 100vh; + width: 100vw; + overflow: hidden; +} + +/* --- LEFT PANEL: Branding --- */ +.karavan-brand-panel { + width: 35%; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: var(--pf-t--color--gray--95); + gap: 48px; +} + +.brand-content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; width: 100%; - height: 100%; + + .brand-name { + display: flex; + flex-direction: column; + justify-content: center; + gap: 20px; + + .brand-logo-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + + .logo { + height: 40px; + width: 40px; + } + .platform-name-text { + font-size: 24px; + font-weight: 700; + letter-spacing: 1px; + } + } + .gradient-text-blue { + background: linear-gradient(to right, #4394e5, #92c5f9); + + -webkit-background-clip: text; /* Safari / Chrome */ + background-clip: text; + color: transparent; + } + .gradient-text-blue-gold { + background: linear-gradient(to right, #fda422, #ec7a08); + + -webkit-background-clip: text; /* Safari / Chrome */ + background-clip: text; + color: transparent; + } + .gradient-text-gold { + background: linear-gradient(to right, #ec7a08, #db5b04); + + -webkit-background-clip: text; /* Safari / Chrome */ + background-clip: text; + color: transparent; + } + + .tagline1 { + font-weight: 800; + font-size: 56px; + line-height: 1.1; + text-align: center; + } + + .tagline2 { + font-weight: 400; + line-height: 1.5; + color: #b9dafc; /* Tblue-200 for subtitle */ + font-size: 20px; + letter-spacing: 0.5px; + margin-top: 16px; + text-align: center; + } + } +} + +.solar-content { display: flex; flex-direction: column; - background-color: var(--pf-t--global--background--color--secondary--default); + justify-content: center; + gap: 8px; + /* Container that holds both the center logo and the ring */ + .solar-system { + position: relative; + width: 350px; /* Width of the whole system */ + height: 350px; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: flex-end; + } } -.karavan .login-page .logo-panel { - background-color: var(--pf-t--color--gray--95); - height: 64px; + +.orbit-lines-svg { + position: absolute; + top: 0; + left: 0; + transform: translate(-50%, -50%); + width: 350px; /* Must match SVG viewBox width */ + height: 350px; /* Must match SVG viewBox height */ + z-index: 1; /* Lowest level, behind icons */ + pointer-events: none; /* Let clicks pass through to the logo */ + + /* OPTIONAL: Rotate the gradient slowly for extra "flash" */ + animation: slowSpin 20s linear infinite; +} + +/* Ensure your icons are on top */ +.orbit-ring { + z-index: 5; +} + +/* 1. THE STATIC CENTER LOGO */ +.static-sun { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); /* Perfectly centered */ + width: 100px; + height: 100px; + z-index: 10; /* Ensures it sits ON TOP of the ring lines */ + + /* Optional: A nice glow behind the main logo */ + background: radial-gradient( + circle, + rgba(236, 122, 8, 0.4) 0%, + rgba(236, 122, 8, 0) 60% + ); + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; } -.karavan .login-page .login { - width: 470px; +.static-sun a { + pointer-events: auto; + width: 48px; /* Your actual logo size */ + height: 48px; + display: flex; + justify-content: center; + align-items: center; + transition: transform 0.3s ease, filter 0.3s ease; + &:hover { + transform: scale(1.1); + } } -.karavan .login-page .logo { - /*height: 48px;*/ -} \ No newline at end of file +.static-sun .logo { + width: 100%; /* Adjust to fit inside the glow */ + height: auto; + filter: none; +} + +/* 1. The Ring spins clockwise */ +.orbit-ring { + width: 100%; + height: 100%; + position: absolute; + animation: slowSpin 60s linear infinite; +} + +/* 2. The Item is placed on the ring (Static Position) */ +.orbit-item { + position: absolute; + top: 50%; + left: 50%; + width: 32px; /* Adjusted size */ + height: 32px; + margin-top: -16px; + margin-left: -16px; + + /* Logic: Rotate to angle -> Move out -> Rotate back to upright */ + transform: + rotate(var(--angle)) + translate(var(--radius)) + rotate(calc(var(--angle) * -1)); +} + +/* 3. The Counter-Rotator spins Counter-Clockwise + This cancels out the ring's rotation so the icon always faces North */ +.counter-rotator { + width: 100%; + height: 100%; + animation: slowSpinReverse 60s linear infinite; +} + +/* 4. The Image handles ONLY the look and hover scale */ +.counter-rotator img, +.counter-rotator svg { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + + /* Look */ + /*filter: grayscale(100%);*/ + transition: transform 0.3s ease, filter 0.3s ease; +} + +/* Hover Effect: Scale the image, NOT the rotating wrapper */ +.counter-rotator:hover img, +.counter-rotator:hover svg { + filter: none; + transform: scale(1.2); +} + +/* --- Keyframes --- */ +@keyframes slowSpin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes slowSpinReverse { + /* Must match the ring's speed but in REVERSE */ + from { transform: rotate(360deg); } + to { transform: rotate(0deg); } +} + +.anchor-line { + width: 1px; + height: 50px; /* Length of line */ + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 10%, rgba(236, 122, 8, 1) 50%, rgba(255, 255, 255, 0.1) 100%); + margin: 0 auto 8px auto; /* Center it and add space below */ +} + +.brand-footer { + opacity: 0.6; + font-size: 0.85rem; + letter-spacing: 1px; + text-transform: uppercase; + text-align: center; + color: var(--pf-t--color--blue--10); + padding-bottom: 60px; +} + +/* --- RIGHT PANEL: Minimal Form --- */ +.karavan-form-panel { + width: 65%; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; +} + +.form-wrapper { + width: 100%; + max-width: 400px; + padding: 20px; + + .login { + &::before { + position: absolute; + inset: 0; + pointer-events: none; + content: ""; + border-color: var(--pf-t--color--blue--60); + border-style: var(--pf-v6-c-card--BorderStyle); + border-width: var(--pf-v6-c-card--BorderWidth); + border-radius: inherit; + } + + .login-header { + color: var(--pf-t--global--text--color--subtle); + } + + .environment-dev { + background-color: var(--pf-t--global--text--color--brand--default); + } + .environment-default { + background-color: var(--pf-t--global--icon--color--status--warning--default); + } + .environment-prod { + background-color: var(--pf-t--global--icon--color--status--danger--default); + } + } + + .pf-v6-c-form-control, .pf-v6-c-text-input-group { + /*background-color: transparent;*/ + } + + .pf-v6-c-card__header, .pf-v6-c-card__title, .pf-v6-c-card__body { + padding-block-end: var(--pf-t--global--spacer--xl); + } + + .pf-v6-c-card__footer { + padding-block-end: var(--pf-t--global--spacer--md); + } + + .platform-version { + color: var(--pf-t--global--text--color--subtle); + font-size: var(--pf-t--global--font--size--xs); + } + + .button { + width: 100%; + } + + .button-disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.powered-by-logo { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + + + .logo, .icon{ + width: 32px; + height: 32px; + } + .openapi-logo { + height: 36px; + } + .asyncapi-logo { + height: 32px; + } + .json-schema-logo { + height: 28px; + } + .groovy-logo { + height: 32px; + } + .jib-logo { + height: 32px; + } + .infinispan-logo { + height: 30px; + } + .jkube-logo { + height: 32px; + } + .patternfly-logo { + height: 28px; + } +} diff --git a/karavan-app/src/main/webui/src/karavan/app/login/LoginPage.tsx b/karavan-app/src/main/webui/src/karavan/app/login/LoginPage.tsx index 1d2cefc0..aeac2333 100644 --- a/karavan-app/src/main/webui/src/karavan/app/login/LoginPage.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/login/LoginPage.tsx @@ -1,12 +1,12 @@ -import React, {useContext} from 'react'; +import React, {useContext, useEffect} from 'react'; import { - ActionGroup, Alert, - Bullseye, Button, Card, CardBody, - CardTitle, + CardFooter, + CardHeader, + Checkbox, Content, Form, FormAlert, @@ -19,11 +19,15 @@ import { import './LoginPage.css' import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; -import DarkModeToggle from "../theme/DarkModeToggle"; -import {MainToolbar} from "@shared/ui/MainToolbar"; import {AuthContext} from "@api/auth/AuthProvider"; import {AuthApi} from "@api/auth/AuthApi"; -import {KaravanIcon} from "@features/integration/designer/icons/KaravanIcons"; +import {CamelIcon, KaravanIcon, OpenApiIcon} from "@features/project/designer/icons/KaravanIcons"; +import {SvgIcon} from "@shared/icons/SvgIcon"; +import jibLogo from '@shared/icons/jib.png'; +import {PlatformNameForToolbar, PlatformVersion} from "@shared/ui/PlatformLogos"; +import PlatformLogo from "@app/navigation/PlatformLogo"; +import OrbitLines from "@app/login/OrbitLines"; +import {useReadinessStore} from "@stores/ReadinessStore"; export const LoginPage: React.FunctionComponent = () => { @@ -33,19 +37,31 @@ export const LoginPage: React.FunctionComponent = () => { const [showError, setShowError] = React.useState(false); const [error, setError] = React.useState(''); const {reload} = useContext(AuthContext); + const { readiness } = useReadinessStore(); + + const [titleSvg, setTitleSvg] = React.useState<string>(); + + useEffect(() => { + }, []); function onLoginButtonClick(event: any) { - event.preventDefault(); - AuthApi.login(username, password, (ok, res) => { - if (!ok) { - setError(res?.response?.data); - setShowError(true); - } else { - setError(''); - setShowError(false); - reload(); - } - }) + if (!getButtonDisabled()) { + event.preventDefault(); + AuthApi.login(username, password, (ok, res) => { + if (!ok) { + setError(res?.response?.data); + setShowError(true); + } else { + setError(''); + setShowError(false); + reload(); + } + }) + } + } + + function getButtonDisabled(): boolean { + return (username?.length < 3 || password?.length < 3); } function onKeyDown(event: React.KeyboardEvent<HTMLFormElement>): void { @@ -56,24 +72,74 @@ export const LoginPage: React.FunctionComponent = () => { } } + + function getLogos() { + return [ + <div className="powered-by-logo counter-rotator"> + <a href="https://github.com/apache/camel-karavan" target="_blank">{KaravanIcon("logo")}</a> + </div>, + <div className="powered-by-logo counter-rotator"> + <a href="https://www.openapis.org/" target="_blank"><OpenApiIcon className={"logo"}/></a> + </div>, + <div className="powered-by-logo counter-rotator"> + <a href="https://www.asyncapi.com/" target="_blank">{SvgIcon({icon: 'asyncapi', className: 'asyncapi-logo'})}</a> + </div>, + <div className="powered-by-logo counter-rotator"> + <a href="https://json-schema.org/" target="_blank"> + {SvgIcon({icon: 'json-schema-dark', className: 'json-schema-logo'})} + </a> + </div>, + // <div className="powered-by-logo counter-rotator"> + // <a href="https://groovy-lang.org/" target="_blank">{SvgIcon({icon: 'groovy', className: 'groovy-logo'})}</a> + // </div>, + <div className="powered-by-logo counter-rotator"> + <a href="https://github.com/GoogleContainerTools/jib" target="_blank"> + <img src={jibLogo} alt="Logo" className="jib-logo"/> + </a> + </div>, + <div className="powered-by-logo counter-rotator"> + <a href="https://camel.apache.org/" target="_blank">{CamelIcon()}</a> + </div>, + <div className="powered-by-logo counter-rotator"> + <a href="https://www.patternfly.org/" target={'_blank'}> + {SvgIcon({icon: 'patternfly', className: 'patternfly-logo'})} + </a> + </div>, + <div className="powered-by-logo counter-rotator"> + <a href="https://eclipse.dev/jkube/" target="_blank"> + {SvgIcon({icon: 'jkube', className: 'jkube-logo'})} + </a> + </div>, + <div className="powered-by-logo counter-rotator"> + <a href="https://www.keycloak.org/" target="_blank"> + {SvgIcon({icon: 'keycloak', className: 'patternfly-logo'})} + </a> + </div> + ]; + } + + const LOGOS = getLogos(); + function getLoginForm() { return ( <Form onKeyDown={onKeyDown}> - <FormGroup fieldId="username" label="Username" isRequired> + <FormGroup fieldId="username"> <TextInput className="text-field" type="text" id="username" name="username" value={username} + placeholder={"Username"} onChange={(_, value) => setUsername(value)}/> </FormGroup> - <FormGroup fieldId="password" label="Password" isRequired> + <FormGroup fieldId="password"> <TextInputGroup> <TextInputGroupMain className="text-field" type={passwordHidden ? "password" : 'text'} id="password" name="password" value={password} + placeholder={"Password"} onChange={(_, value) => setPassword(value)} /> <TextInputGroupUtilities> @@ -87,14 +153,6 @@ export const LoginPage: React.FunctionComponent = () => { </TextInputGroupUtilities> </TextInputGroup> </FormGroup> - <ActionGroup> - <Button variant="primary" - style={{width: '100%'}} - onClick={onLoginButtonClick} - > - Login - </Button> - </ActionGroup> {showError && ( <FormAlert> <Alert variant="danger" title={<div>{error?.toString()}</div>} aria-live="polite" isInline/> @@ -104,29 +162,88 @@ export const LoginPage: React.FunctionComponent = () => { ) } - return ( - <div className='login-page'> - <div className="logo-panel"> - <MainToolbar title={<></>} toolsStart={<></>} tools={ - <div id="toolbar-group-types" style={{display: 'flex', alignItems: 'center', gap: '8px', height: '65px'}}> - {KaravanIcon()} - <Content component='h1' style={{color: 'var(--pf-t--color--blue--30)'}}>Apache Camel Karavan</Content> - </div> - }/> + + function getRightSide() { + const buttonClassName = getButtonDisabled() ? "button button-disabled" : "button"; + return ( + <div className="karavan-form-panel dark-form"> + <div className="form-wrapper"> + <Card className="login" isLarge> + <CardHeader> + <div style={{display: "flex", flexDirection: 'row', justifyContent: 'space-between', alignItems: "center"}}> + <Content component='h3' className="login-header">Login</Content> + <PlatformVersion environment={readiness?.environment}/> + </div> + </CardHeader> + <CardBody> + {getLoginForm()} + </CardBody> + <CardFooter style={{ textAlign: "center" }}> + <Button variant="primary" + className={buttonClassName} + onClick={onLoginButtonClick} + > + Access Platform + </Button> + </CardFooter> + </Card> + {/*<DarkModeToggle/>*/} + </div> </div> - <Bullseye> - <Card className="login"> - <CardTitle> - <Content component="h2">Login</Content> - </CardTitle> - <CardBody> - {getLoginForm()} - </CardBody> - </Card> - </Bullseye> - <div style={{padding: 16}}> - <DarkModeToggle/> + ) + } + + function getLeftSide() { + return ( + <div className="karavan-brand-panel"> + <div className="brand-content"> + <div className="brand-name"> + <div className="brand-logo-container"> + {PlatformLogo("logo")} + <span className="platform-name-text">{PlatformNameForToolbar()}</span> + </div> + <div> + <div className="tagline1 gradient-text-blue">Accelerate</div> + <div className="tagline1 gradient-text-blue-gold">Integration</div> + <div className="tagline1 gradient-text-gold">Development</div> + </div> + <Content component='p' className="tagline2">Unified Design and Runtime for <br/> APIs • Events • Data Pipelines</Content> + </div> + </div> + <div className="solar-content"> + <div className="solar-system"> + <OrbitLines /> + <div className="static-sun"> + <a href="" target="_blank"> + {PlatformLogo("logo")} + </a> + </div> + <div className="orbit-ring"> + {LOGOS.map((logo, index) => { + const total = LOGOS.length; + const angle = (360 / total) * index; + // Increased radius slightly to make room for the center logo + const radius = 150; + const style = {'--angle': `${angle}deg`, '--radius': `${radius}px`,} as React.CSSProperties; + return ( + <div key={index} className="orbit-item" style={style}> + {logo} + </div> + ); + })} + </div> + <div className="anchor-line"></div> + <p className="brand-footer">Powered by</p> + </div> + </div> </div> + ) + } + + return ( + <div className="karavan-container"> + {getLeftSide()} + {getRightSide()} </div> ) } \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/karavan/app/login/UserPopupOidc.tsx b/karavan-app/src/main/webui/src/karavan/app/login/UserPopupOidc.tsx index 79b8b472..1498d6a9 100644 --- a/karavan-app/src/main/webui/src/karavan/app/login/UserPopupOidc.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/login/UserPopupOidc.tsx @@ -1,6 +1,5 @@ import React from 'react'; import {Badge, Content, ContentVariants, DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, Flex, Popover,} from '@patternfly/react-core'; -import '../App.css'; import UserIcon from "@patternfly/react-icons/dist/esm/icons/user-icon"; import {shallow} from "zustand/shallow"; import {useAccessStore} from "@stores/AccessStore"; diff --git a/karavan-app/src/main/webui/src/karavan/app/navigation/MainRoutes.tsx b/karavan-app/src/main/webui/src/karavan/app/navigation/MainRoutes.tsx index 5e5822bb..799d4293 100644 --- a/karavan-app/src/main/webui/src/karavan/app/navigation/MainRoutes.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/navigation/MainRoutes.tsx @@ -1,3 +1,19 @@ +/* + * 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 from "react"; import {NotAuthorizedPage} from "@app/navigation/NotAuthorizedPage"; @@ -5,13 +21,19 @@ import {SystemPage} from "@features/system/SystemPage"; import {AccessPage} from "@features/access/AccessPage"; import {DocumentationPage} from "@features/documentation/DocumentationPage"; import {ROUTES} from "./Routes"; +import {AsyncPage} from "@features/landscape/AsyncPage"; import {ProtectedRoute} from "@app/navigation/ProtectedRoute"; -import {LoginPage} from "@app/login/LoginPage"; import {ProjectFunctionHook} from "@app/navigation/ProjectFunctionHook"; -import {ProjectProvider} from "@features/integration/ProjectContext"; -import {DeveloperManager} from "@features/integration/developer/DeveloperManager"; -import ProjectPage from "@features/integration/ProjectPage"; -import {IntegrationsPage} from "@features/integrations/IntegrationsPage"; +import {ProjectProvider} from "@features/project/ProjectContext"; +import {DeveloperManager} from "@features/project/developer/DeveloperManager"; +import ProjectPage from "@features/project/ProjectPage"; +import {ProjectsPage} from "@features/projects/ProjectsPage"; +import {SharedSchemasPage} from "@features/schemas/SharedSchemasPage"; +import {DashboardPage} from "@features/dashboard/DashboardPage"; +import {OpenApiPage} from "@features/apis/OpenApiPage"; +import {LoginPage} from "@app/login/LoginPage"; +import {useReadinessStore} from "@stores/ReadinessStore"; +import {SettingsPage} from "@features/settings/SettingsPage"; export function MainRoutes() { @@ -22,32 +44,47 @@ export function MainRoutes() { <LoginPage/> </ProtectedRoute>} /> - <Route path={ROUTES.INTEGRATIONS} element={ + <Route path={ROUTES.PROJECTS} element={ <ProtectedRoute> <ProjectProvider useProjectHook={ProjectFunctionHook}> - <IntegrationsPage key="integrations"/> + <ProjectsPage key="integrations"/> </ProjectProvider> </ProtectedRoute> }/> - <Route path={ROUTES.INTEGRATION_DETAIL} element={ + <Route path={ROUTES.PROJECT_DETAIL} element={ <ProtectedRoute> <ProjectProvider useProjectHook={ProjectFunctionHook}> <ProjectPage key="project" developerManager={<DeveloperManager/>}/> </ProjectProvider> </ProtectedRoute> }/> - <Route path={ROUTES.INTEGRATION_FILE} element={ + <Route path={ROUTES.PROJECT_FILE} element={ <ProtectedRoute> <ProjectProvider useProjectHook={ProjectFunctionHook}> <ProjectPage key="project" developerManager={<DeveloperManager/>}/> </ProjectProvider> </ProtectedRoute> }/> + <Route path={ROUTES.SETTINGS} element={ + <ProtectedRoute> + <ProjectProvider useProjectHook={ProjectFunctionHook}> + <SettingsPage/> + </ProjectProvider> + </ProtectedRoute> + }/> + <Route path={ROUTES.SETTINGS_FILE} element={ + <ProtectedRoute> + <ProjectProvider useProjectHook={ProjectFunctionHook}> + <SettingsPage/> + </ProjectProvider> + </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.INTEGRATIONS} replace/>}/> + {/*{readiness?.environment === 'dev' && <Route path="*" element={<Navigate to={ROUTES.PROJECTS} replace/>}/>}*/} + <Route path="*" element={<Navigate to={ROUTES.DASHBOARD} replace/>}/> </Routes> ) } \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/karavan/app/navigation/NavigationMenu.tsx b/karavan-app/src/main/webui/src/karavan/app/navigation/NavigationMenu.tsx index 7d47fb8a..6ee1f728 100644 --- a/karavan-app/src/main/webui/src/karavan/app/navigation/NavigationMenu.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/navigation/NavigationMenu.tsx @@ -1,9 +1,24 @@ +/* + * 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 {AuthApi, getCurrentUser} from "@api/auth/AuthApi"; import React from "react"; -import {CamelIcon} from "@features/integration/designer/icons/KaravanIcons"; import {SvgNavigationIcon} from "@shared/icons/SvgNavigationIcon"; -import {KubernetesIcon} from "@features/integration/designer/icons/ComponentIcons"; -import DockerIcon from "@patternfly/react-icons/dist/esm/icons/docker-icon"; +import {KubernetesIcon} from "@features/project/designer/icons/ComponentIcons"; +import {DockerIcon} from "@patternfly/react-icons"; export class MenuItem { pageId: string = ''; @@ -17,26 +32,33 @@ export class MenuItem { } } -export function getNavigationMenu(environment: string, infrastructure: string): MenuItem[] { +export function getNavigationFirstMenu(): MenuItem[] { + return [ + new MenuItem("projects", "Projects", SvgNavigationIcon({icon: 'apps'})), + new MenuItem("settings", "Settings", SvgNavigationIcon({icon: 'settings'})), + ] +} + + +export function getNavigationSecondMenu(environment: string, infrastructure: string): MenuItem[] { const iconInfra = infrastructure === 'kubernetes' ? KubernetesIcon("infra-icon-k8s") : <DockerIcon className='infra-icon-docker'/>; - const pages: MenuItem[] = [ - new MenuItem("integrations", "Integrations", <CamelIcon />), - ] - // if (environment === 'dev') { - // pages.push(new MenuItem("services", "Services", <ServicesIcon/>)) - // } + 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', width: 24, height: 24}))); - } - if (environment === 'dev') { - pages.push(new MenuItem("documentation", "Docs", SvgNavigationIcon({icon: 'documentation', width: 24, height: 24}))); + 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/karavan/app/navigation/NotAuthorizedPage.tsx b/karavan-app/src/main/webui/src/karavan/app/navigation/NotAuthorizedPage.tsx index 33e01bd9..95db9777 100644 --- a/karavan-app/src/main/webui/src/karavan/app/navigation/NotAuthorizedPage.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/navigation/NotAuthorizedPage.tsx @@ -1,3 +1,19 @@ +/* + * 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, {useEffect} from "react"; import {Bullseye, EmptyState, EmptyStateBody, EmptyStateVariant} from "@patternfly/react-core"; import NotAuthorizedIcon from "@patternfly/react-icons/dist/esm/icons/user-secret-icon"; diff --git a/karavan-app/src/main/webui/src/karavan/app/navigation/PageNavigation.css b/karavan-app/src/main/webui/src/karavan/app/navigation/PageNavigation.css index 73acddf3..a5d0ce44 100644 --- a/karavan-app/src/main/webui/src/karavan/app/navigation/PageNavigation.css +++ b/karavan-app/src/main/webui/src/karavan/app/navigation/PageNavigation.css @@ -1,38 +1,74 @@ +/* + * 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. + */ + .karavan .nav-buttons { width: fit-content; display: flex; flex-direction: column; height: 100%; justify-content: space-between; - background-color: var(--pf-t--color--gray--95); + background: var(--pf-t--color--gray--95); } -.karavan .nav-buttons .nav-button-part-wrapper { + +.karavan .nav-buttons .nav-button-part-wrapper, +.karavan .nav-buttons .nav-button-part-darkmode { display: flex; flex-direction: column; align-items: center; - height: 64px; position: relative; - .logo { - margin-left: 3px; - width: 48px; - height: 48px; - } } .karavan .nav-buttons .environment-wrapper { display: flex; - flex-direction: row; + flex-direction: column; justify-content: center; align-items: center; - margin-bottom: 6px; - margin-top: 6px; + margin-bottom: 16px; + gap: 3px; + color: var(--pf-t--color--blue--10); + /*margin-top: 6px;*/ + .version { color: var(--pf-t--color--gray--30); } + .environment { - border-color: var(--pf-t--color--gray--60); - color: var(--pf-t--color--white); - background-color: var(--pf-t--color--gray--60); + padding: 0 4px 0 4px; + border-color: #4bb9ecff; + color: var(--pf-t--color--gray--95); + background-color: #4bb9ecff; + font-weight: normal; + } + + .environment-line { + display: flex; + flex-direction: row; + gap: 3px; + align-items: center; + } + + .login-logo { + width: 18px; + height: 18px; + } + + .icon { + width: 16px; + height: 16px; } } @@ -42,33 +78,21 @@ width: 32px; height: 32px; } -.karavan .nav-buttons .pf-v6-c-button { - padding: 0; - /*width: fit-content;*/ - height: 64px; - /*color: var(--pf-t-global--color--light-100);*/ -} .karavan .nav-buttons .pf-v6-c-button svg { - width: 24px; - height: 24px; - /*fill: var(--pf-t--color--white);*/ + width: 16px; + height: 16px; + fill: #4bb9ec; } /* Adapt navigation for screens less than 800px */ @media screen and (max-height: 800px) { - .karavan .nav-buttons .pf-v6-c-button { - height: 50px; - } - .karavan .nav-buttons .pf-v6-c-button .pf-v6-c-button__icon { - /*display: none;*/ + .karavan .nav-buttons .nav-button { + /*padding: 16px 12px 16px 12px;*/ } - .karavan .nav-buttons .pf-v6-c-button .nav-button-badge { - top: 2px; - right: 2px; - } - .karavan .nav-buttons .pf-v6-c-button .nav-button-badge .pf-v6-c-badge { - font-size: 10px; + + .karavan .nav-buttons .pf-v6-c-button { + font-size: 12px; } } @@ -81,26 +105,38 @@ background-color: var(--pf-t--color--gray--60); } -.karavan .nav-buttons .nav-button-wrapper { - .infra-icon-docker { - fill: #1074FF; - } -} .karavan .nav-buttons .nav-button { - border-left-width: 3px; - border-left-style: solid; - border-left-color: transparent; border-top-width: 1px; border-top-style: solid; border-top-color: var(--pf-t--color--gray--80); border-radius: 0; display: flex; - flex-direction: column; - justify-content: center; + flex-direction: row; + justify-content: flex-start; align-items: center; - gap: 0; - padding: 0 9px 0 6px; - color: var(--pf-t--color--white); + gap: 4px; + padding: 10px 10px 10px 10px; + color: var(--pf-t--color--blue--10); + &:hover { + background: color-mix(in srgb, var(--pf-t--color--blue--60) 50%, transparent); + } + + .pf-v6-c-button__text { + + } + .nav-button-badge { + position: absolute; + top: 3px; + right: 3px; + margin: 0; + .pf-v6-c-badge { + padding: 0 6px; + min-width: fit-content; + background-color: var(--pf-t--color--orange--40); + color: var(--pf-t--color--gray--95); + font-weight: normal; + } + } } .karavan .nav-buttons .nav-button .pf-v6-c-button__icon.pf-m-start { @@ -114,21 +150,41 @@ } .karavan .nav-button-selected .pf-v6-c-button { - border-left-color: #ec7a08; - background-color: var(--pf-t--color--gray--60); -} + background: rgba(236, 122, 8, 0.25); + &:hover { + /*background: color-mix(in srgb, var(--pf-t--color--blue--60) 50%, transparent);*/ + } -.karavan .nav-buttons .nav-button-badge { - position: absolute; - top: 5px; - right: 5px; - margin: 0; + .nav-button-badge { + .pf-v6-c-badge { + padding: 0 6px; + min-width: fit-content; + color: var(--pf-t--color--orange--40); + background-color: var(--pf-t--color--gray--95); + font-weight: 500; + } + } + + &::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + border-left: 2px solid rgba(236, 122, 8, 1); + pointer-events: none; + } } -.karavan .nav-buttons .nav-button-badge .pf-v6-c-badge { - padding: 0 6px; - min-width: fit-content; - background-color: var(--pf-t--color--orange--30); - color: var(--pf-t--color--gray--95); - font-weight: normal; +.karavan .nav-buttons { + .dark-mode-toggle { + display: flex; + flex-direction: row; + justify-content: center; + width: 100%; + padding: 12px; + border-top-width: 1px; + border-top-style: solid; + border-top-color: var(--pf-t--color--gray--80); + border-radius: 0; + } } \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/karavan/app/navigation/PageNavigation.tsx b/karavan-app/src/main/webui/src/karavan/app/navigation/PageNavigation.tsx index db6cae62..5a1a59bf 100644 --- a/karavan-app/src/main/webui/src/karavan/app/navigation/PageNavigation.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/navigation/PageNavigation.tsx @@ -1,8 +1,23 @@ +/* + * 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, useState} from 'react'; import {Badge, Button,} from '@patternfly/react-core'; import './PageNavigation.css'; -import LogoutIcon from "@patternfly/react-icons/dist/esm/icons/door-open-icon"; -import {useAppConfigStore, useDevModeStore, useFileStore, useProjectsStore, useStatusesStore} from "@stores/ProjectStore"; +import {useAppConfigStore, useDevModeStore, useFileStore} from "@stores/ProjectStore"; import {shallow} from "zustand/shallow"; import {useLocation, useNavigate} from "react-router-dom"; import {SsoApi} from "@api/auth/SsoApi"; @@ -10,29 +25,26 @@ import {UserPopupOidc} from "../login/UserPopupOidc"; import {BUILD_IN_PROJECTS} from "@models/ProjectModels"; import DarkModeToggle from "@app/theme/DarkModeToggle"; import {AuthApi} from "@api/auth/AuthApi"; -import {getNavigationMenu} from "@app/navigation/NavigationMenu"; +import {getNavigationFirstMenu, getNavigationSecondMenu, MenuItem} from "@app/navigation/NavigationMenu"; import {AuthContext} from "@api/auth/AuthProvider"; -import {KaravanIcon} from "@features/integration/designer/icons/KaravanIcons"; +import {PlatformLogoBase64} from "@app/navigation/PlatformLogo"; +import {PlatformVersions} from "@shared/ui/PlatformLogos"; - -export function PageNavigation() { +function PageNavigation() { const [config] = useAppConfigStore((s) => [s.config], shallow) - const menu = getNavigationMenu(config.environment, config.infrastructure); + const firstMenu = getNavigationFirstMenu(); + const secondMenu = getNavigationSecondMenu(config.environment, config.infrastructure); const [setFile] = useFileStore((state) => [state.setFile], shallow) const [setStatus, setPodName] = useDevModeStore((state) => [state.setStatus, state.setPodName], shallow) - const [projects] = useProjectsStore((s) => [s.projects], shallow) - const [deployments, containers] = useStatusesStore((state) => [state.deployments, state.containers], shallow) - const [pageId, setPageId] = useState<string>(menu?.at(0)?.pageId || 'integrations'); + const [pageId, setPageId] = useState<string>(); const navigate = useNavigate(); const location = useLocation(); const {reload} = useContext(AuthContext); - const projectCount = projects.filter(p => !BUILD_IN_PROJECTS.includes(p.projectId))?.length; - React.useEffect(() => { var page = location.pathname?.split("/").filter(Boolean)[0]; - if (page === 'integrations') { + if (page === 'projects') { var projectId = location.pathname?.split("/").filter(Boolean)[1]; if (BUILD_IN_PROJECTS.includes(projectId)) { setPageId('settings'); @@ -41,66 +53,79 @@ export function PageNavigation() { } } else if (page !== undefined) { setPageId(page); + } else if (config.environment === 'dev') { + setPageId('projects'); } else { - setPageId('integrations'); + setPageId('projects'); } }, [location]); + function onClick(page: MenuItem) { + if (page.pageId === 'logout') { + if (AuthApi.authType === 'oidc') { + SsoApi.logout(() => { + }); + } else if (AuthApi.authType === 'session') { + AuthApi.logout(); + reload(); + } + } else { + setFile('none', undefined); + setPodName(undefined); + setStatus("none"); + setPageId(page.pageId); + navigate(page.pageId); + } + } + + function getMenu(menu: MenuItem[]) { + return ( + menu.map((page, index) => { + let className = "nav-button"; + const isSelected = pageId === page.pageId; + className = className.concat(isSelected ? " nav-button-selected" : ""); + return ( + <div key={page.pageId} className={isSelected ? "nav-button-selected nav-button-wrapper" : "nav-button-wrapper"}> + <Button id={page.pageId} + style={{width: '100%'}} + variant={"link"} + className={className} + // countOptions={badge} + onClick={_ => onClick(page)} + > + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '8px'}}> + {page.icon} + {page.name} + </div> + </Button> + </div> + ) + }) + ) + } return ( <div className="nav-buttons pf-v6-theme-dark"> <div className='nav-button-part-wrapper'> - {KaravanIcon()} + <img src={PlatformLogoBase64()} className="logo" alt='logo'/> </div> <div style={{alignSelf: 'center'}} className='environment-wrapper'> + <Badge isRead className='environment'>{config.environment}</Badge> </div> - {menu.map((page, index) => { - let className = "nav-button"; - className = className.concat(pageId === page.pageId ? " nav-button-selected" : ""); - className = className.concat((index === menu.length - 1) ? " nav-button-last" : ""); - return ( - <div key={page.pageId} className={pageId === page.pageId ? "nav-button-selected nav-button-wrapper" : "nav-button-wrapper"}> - <Button id={page.pageId} - style={{width: '100%'}} - icon={page.icon} - variant={"link"} - className={className} - onClick={event => { - setFile('none', undefined); - setPodName(undefined); - setStatus("none"); - setPageId(page.pageId); - navigate(page.pageId); - }} - > - {page.name} - </Button> - </div> - ) - })} - <div className='nav-button-part-wrapper' style={{flexGrow: '2'}}/> + {getMenu(firstMenu)} + <div style={{flex: 2}}/> + {getMenu(secondMenu)} {AuthApi.authType === 'oidc' && <div className='nav-button-part-wrapper'> <UserPopupOidc/> </div> } - - <div className='nav-button-part-wrapper'> + <div className={"dark-mode-toggle"}> <DarkModeToggle/> </div> - <div className='nav-button-part-wrapper'> - <Button icon={<LogoutIcon/>} className={"nav-button"} style={{width: '100%'}} variant={"link"} - onClick={event => { - if (AuthApi.authType === 'oidc') { - SsoApi.logout(() => { - }); - } else if (AuthApi.authType === 'session') { - AuthApi.logout(); - reload(); - } - }} - >Exit</Button> - </div> + <PlatformVersions/> </div> ) -} \ No newline at end of file +} + +export default PageNavigation \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/karavan/app/navigation/Routes.ts b/karavan-app/src/main/webui/src/karavan/app/navigation/Routes.ts index 807c6ce7..c9daa3e8 100644 --- a/karavan-app/src/main/webui/src/karavan/app/navigation/Routes.ts +++ b/karavan-app/src/main/webui/src/karavan/app/navigation/Routes.ts @@ -1,10 +1,27 @@ -// routes.ts +/* + * 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. + */ + export const ROUTES = { - INTEGRATIONS: "/integrations", - INTEGRATION_DETAIL: "/integrations/:projectId", - INTEGRATION_FILE: "/integrations/:projectId/:fileName", - SERVICES: "/services", + 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", diff --git a/karavan-app/src/main/webui/src/karavan/app/theme/DarkModeToggle.tsx b/karavan-app/src/main/webui/src/karavan/app/theme/DarkModeToggle.tsx index ea4910fd..1a50f6c6 100644 --- a/karavan-app/src/main/webui/src/karavan/app/theme/DarkModeToggle.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/theme/DarkModeToggle.tsx @@ -1,3 +1,19 @@ +/* + * 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 { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'; import { SunIcon, MoonIcon } from '@patternfly/react-icons'; import { useTheme } from './ThemeContext'; diff --git a/karavan-app/src/main/webui/src/karavan/app/theme/ThemeContext.tsx b/karavan-app/src/main/webui/src/karavan/app/theme/ThemeContext.tsx index 0eec10e9..90fb6251 100644 --- a/karavan-app/src/main/webui/src/karavan/app/theme/ThemeContext.tsx +++ b/karavan-app/src/main/webui/src/karavan/app/theme/ThemeContext.tsx @@ -1,4 +1,20 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; +/* + * 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, {createContext, useContext, useEffect, useState} from 'react'; interface ThemeContextType { isDark: boolean; @@ -11,22 +27,27 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre const [isDark, setIsDark] = useState(false); useEffect(() => { - const stored = localStorage.getItem('pf-theme'); - if (stored === 'dark') { - setIsDark(true); - document.documentElement.classList.add('pf-v6-theme-dark'); + 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); - if (checked) { - document.documentElement.classList.add('pf-v6-theme-dark'); - localStorage.setItem('pf-theme', 'dark'); - } else { - document.documentElement.classList.remove('pf-v6-theme-dark'); - localStorage.setItem('pf-theme', 'light'); - } + document.documentElement.classList.toggle('pf-v6-theme-dark', checked); + localStorage.setItem('pf-theme', checked ? 'dark' : 'light'); }; return (
