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 20f173dc8e2a0f25e53567f8ea2484877d9ca0e3 Author: Marat Gubaidullin <[email protected]> AuthorDate: Fri Feb 27 16:25:43 2026 -0500 Front-end Shared UI for 4.18.0 --- .../main/webui/src/karavan/shared/monaco-setup.ts | 33 ++++---- .../src/karavan/shared/ui/ContainerButton.tsx | 4 +- .../webui/src/karavan/shared/ui/MainToolbar.css | 25 ++++++ .../webui/src/karavan/shared/ui/MainToolbar.tsx | 13 ++- .../src/karavan/shared/ui/ModalConfirmation.tsx | 29 +++++-- .../webui/src/karavan/shared/ui/PlatformLogos.css | 37 +++++++++ .../webui/src/karavan/shared/ui/PlatformLogos.tsx | 95 ++++++++++++++++++++++ .../webui/src/karavan/shared/ui/RightPanel.css | 6 +- .../webui/src/karavan/shared/ui/RightPanel.tsx | 2 +- .../src/karavan/shared/ui/SideBarFormWrapper.tsx | 84 +++++++++++++++++++ 10 files changed, 300 insertions(+), 28 deletions(-) diff --git a/karavan-app/src/main/webui/src/karavan/shared/monaco-setup.ts b/karavan-app/src/main/webui/src/karavan/shared/monaco-setup.ts index 0b2c167c..397a93f3 100644 --- a/karavan-app/src/main/webui/src/karavan/shared/monaco-setup.ts +++ b/karavan-app/src/main/webui/src/karavan/shared/monaco-setup.ts @@ -1,18 +1,23 @@ -// Core API only (no full bundle) -import 'monaco-editor/esm/vs/editor/editor.api'; +import {loader} from '@monaco-editor/react'; +import * as monaco from 'monaco-editor'; +import {type IRange, Position, Range} from 'monaco-editor'; +import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; +import YamlWorker from 'monaco-yaml/yaml.worker.js?worker'; -// Language services -import 'monaco-editor/esm/vs/language/json/monaco.contribution'; +self.MonacoEnvironment = { + getWorker(_, label) { + if (label === 'json') { + return new jsonWorker(); + } + if (label === 'yaml' ) { + return new YamlWorker() + } + return new editorWorker(); + }, +}; -// Basic languages you need -import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution'; -import 'monaco-editor/esm/vs/basic-languages/java/java.contribution'; -import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution'; -import 'monaco-editor/esm/vs/basic-languages/ini/ini.contribution'; -import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution'; -import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution'; +loader.config({ monaco }); -import {IRange, Position, Range} from "monaco-editor/esm/vs/editor/editor.api"; -// (Optional) If you want YAML schema validation instead of just syntax: -export {Range, Position}; +export { Range, Position }; export type { IRange }; diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/ContainerButton.tsx b/karavan-app/src/main/webui/src/karavan/shared/ui/ContainerButton.tsx index 072fcf29..608540c8 100644 --- a/karavan-app/src/main/webui/src/karavan/shared/ui/ContainerButton.tsx +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/ContainerButton.tsx @@ -23,7 +23,7 @@ import {useSelectedContainerStore} from "@stores/ProjectStore"; import {ContainerStatus, ContainerType} from '@models/ProjectModels'; import DevIcon from "@patternfly/react-icons/dist/esm/icons/dev-icon"; import './ContainerButton.css' -import {BuildIcon, LockIcon, PackageIcon, ServiceIcon, UnknownIcon} from '@patternfly/react-icons'; +import {BuildIcon, LockIcon, PackageIcon, UnknownIcon} from '@patternfly/react-icons'; interface Props { container: ContainerStatus, @@ -41,7 +41,6 @@ export function ContainerButton(props: Props) { const typeIconColor = isRunning ? 'var(--pf-t--global--color--status--success--100)' : 'var(--pf-v6-c-button--m-control--Color)'; const iconMap: Record<ContainerType, ReactElement> = { devmode: <DevIcon color={typeIconColor}/>, - devservice: <ServiceIcon color={typeIconColor}/>, packaged: <PackageIcon color={typeIconColor}/>, internal: <LockIcon color={typeIconColor}/>, build: <BuildIcon color={typeIconColor}/>, @@ -49,7 +48,6 @@ export function ContainerButton(props: Props) { }; const typeMap: Record<ContainerType, string> = { devmode: 'container', - devservice: 'devservice', packaged: 'container', internal: 'internal', build: 'build', diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/MainToolbar.css b/karavan-app/src/main/webui/src/karavan/shared/ui/MainToolbar.css index a0906587..fff0a110 100644 --- a/karavan-app/src/main/webui/src/karavan/shared/ui/MainToolbar.css +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/MainToolbar.css @@ -23,6 +23,31 @@ flex-direction: row; justify-content: flex-start; align-items: center; + + .logo { + width: 32px; + height: 32px; + } + + .pf-v6-c-nav { + .pf-v6-c-nav__item { + .pf-m-current { + position: relative; + background: rgba(236, 122, 8, 0.25); + &::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + border: 1px solid rgba(236, 122, 8, 0.66); + pointer-events: none; + } + } + } + .pf-v6-c-nav__link:hover { + background: color-mix(in srgb, var(--pf-t--color--blue--60) 50%, transparent); + } + } } .karavan .main-toolbar h2 { diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/MainToolbar.tsx b/karavan-app/src/main/webui/src/karavan/shared/ui/MainToolbar.tsx index 29105e3b..0479e77c 100644 --- a/karavan-app/src/main/webui/src/karavan/shared/ui/MainToolbar.tsx +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/MainToolbar.tsx @@ -16,27 +16,32 @@ */ import React from 'react'; -import './MainToolbar.css'; +import '@shared/ui/MainToolbar.css'; +import {useAppConfigStore} from "@stores/ProjectStore"; +import {shallow} from "zustand/shallow"; +import {PlatformVersionWithName} from "@shared/ui/PlatformLogos"; interface Props { title: React.ReactNode; toolsStart?: React.ReactNode; - tools: React.ReactNode; + tools?: React.ReactNode; } export function MainToolbar(props: Props) { const { title, toolsStart, tools } = props; + const [config] = useAppConfigStore((s) => [s.config], shallow) + const titleSvg = config?.advanced.titleSvg ? config?.advanced.titleSvg : undefined; return ( <div className="main-toolbar dark-toolbar"> - {title} + {/*{title}*/} {toolsStart && <div style={{flex: 2}}> {toolsStart} </div> } - {tools} + {tools || PlatformVersionWithName(titleSvg)} </div> ) } diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/ModalConfirmation.tsx b/karavan-app/src/main/webui/src/karavan/shared/ui/ModalConfirmation.tsx index 3f35df58..c873965b 100644 --- a/karavan-app/src/main/webui/src/karavan/shared/ui/ModalConfirmation.tsx +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/ModalConfirmation.tsx @@ -6,7 +6,7 @@ * (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 + * 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, @@ -15,7 +15,7 @@ * limitations under the License. */ -import React from 'react'; +import React, {useEffect, useRef} from 'react'; import {Button, Modal, ModalBody, ModalFooter, ModalHeader} from '@patternfly/react-core'; export interface ModalConfirmationProps { @@ -34,9 +34,23 @@ export interface ModalConfirmationProps { export function ModalConfirmation(props: ModalConfirmationProps) { const {title, isOpen, message, onConfirm, onCancel, btnConfirm, btnCancel, btnConfirmVariant, btnCancelVariant, variant} = props; + + // 1. Create a ref for the button + const confirmBtnRef = useRef<HTMLButtonElement>(null); + + // 2. Focus the button when the modal opens + useEffect(() => { + if (isOpen) { + // A small timeout ensures the modal DOM is ready before focusing + const timer = setTimeout(() => { + confirmBtnRef.current?.focus(); + }, 100); + return () => clearTimeout(timer); + } + }, [isOpen]); + return ( <Modal - // className="modal-confirm" variant={variant ?? "small"} isOpen={isOpen} onClose={() => onCancel()} @@ -46,7 +60,12 @@ export function ModalConfirmation(props: ModalConfirmationProps) { {message} </ModalBody> <ModalFooter> - <Button key="confirm" variant={btnConfirmVariant || 'primary'} onClick={event => onConfirm()}> + <Button + ref={confirmBtnRef} // 3. Attach the ref here + key="confirm" + variant={btnConfirmVariant || 'primary'} + onClick={event => onConfirm()} + > {btnConfirm || 'Confirm'} </Button> <Button key="cancel" variant={btnCancelVariant || 'secondary'} onClick={e => onCancel()}> @@ -55,4 +74,4 @@ export function ModalConfirmation(props: ModalConfirmationProps) { </ModalFooter> </Modal> ) -} +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/PlatformLogos.css b/karavan-app/src/main/webui/src/karavan/shared/ui/PlatformLogos.css new file mode 100644 index 00000000..9c38a052 --- /dev/null +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/PlatformLogos.css @@ -0,0 +1,37 @@ +.platform-versions { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + + .pf-v6-c-label { + padding: 4px; + } +} + +.platform-versions { + +} + +.platform-versions-item { + display: flex; + flex-direction: row; + gap: 3px; + align-items: center; + + .platform-version { + font-size: var(--pf-t--global--font--size--body--sm); + line-height: var(--pf-t--global--font--size--body--sm); + color: var(--pf-t--color--gray--30); + } + + .platform-small-logo { + width: 12px; + height: 12px; + } + + .icon { + width: 12px; + height: 12px; + } +} diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/PlatformLogos.tsx b/karavan-app/src/main/webui/src/karavan/shared/ui/PlatformLogos.tsx new file mode 100644 index 00000000..b6805abe --- /dev/null +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/PlatformLogos.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import {Badge, Label} from "@patternfly/react-core"; +import PlatformLogo from "@app/navigation/PlatformLogo"; +import './PlatformLogos.css' + +export const KARAVAN_PLATFORM_VERSION = "4.18.0"; + +interface PlatformVersionProps { + environment: string + short?: boolean +} + +export function PlatformVersion(props: PlatformVersionProps) { + const {environment, short} = props; + const full = short !== true; + const badgeClassName = environment === 'dev' ? 'environment-dev' + : (environment === 'prod' ? 'environment-prod' : 'var(environment-default)'); + + return ( + <div className="platform-versions"> + <Label variant='outline' color='blue'> + <div className='platform-versions-item'> + {full && PlatformLogo("platform-small-logo")} + <p className='platform-version'>{KARAVAN_PLATFORM_VERSION}</p> + <Badge className={badgeClassName}>{environment || ''}</Badge> + </div> + </Label> + </div> + ) +} + +export function PlatformVersions() { + return ( + <div style={{display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: "center", gap: 6, paddingBottom: 6}}> + <div className='platform-versions-item'> + {PlatformLogo("platform-small-logo")} + <p style={{fontSize: '12px', color: 'var(--pf-t--color--gray--30)'}}>{KARAVAN_PLATFORM_VERSION}</p> + </div> + </div> + ) +} + +export function PlatformVersionWithName() { + const brand = ( + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8}}> + <div> + {PlatformName(20)} + </div> + </div> + ) + + return (<div style={{ + padding: '0', + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'end', + }}> + {brand} + </div>) +} + +export function PlatformNameForToolbar() { + return (<div style={{ + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + gap: '3px', + }}> + {PlatformName()} + </div>) +} + +export const PlatformName = (height: number | string = 20, width: number | string = 292.797) => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width={width} + height={height} + preserveAspectRatio="xMidYMid" + viewBox="0 0 292 20" + > + <path + d="M44.653 161.954V26.52H0V3.287h114.694V26.52H70.04v135.434Zm68.34 0L174.647 3.287h27.54l63.24 158.667h-28.673l-14.62-37.853h-67.207l-14.054 37.853zm48.507-59.727h53.834l-27.427-69.133Zm126.934 59.727V3.287h25.386v135.32h69.134v23.347zm123.307 0V3.287h25.386v158.667Zm118.433 3.4q-16.433 0-32.186-4.647-15.64-4.76-26.407-13.6l12.467-19.38q5.666 4.874 13.146 8.387 7.594 3.513 16.094 5.44 8.5 1.813 17 1.813 15.526 0 25.046-5.893 9.634-5.893 9.634-17.227 0-8.5-6.574-14.393-6.46-5 [...] + aria-label="KARAVAN" + style={{ + fill: "#06c", + strokeWidth: 5.3487, + }} + transform="scale(.12095)" + /> + </svg> +) diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/RightPanel.css b/karavan-app/src/main/webui/src/karavan/shared/ui/RightPanel.css index e97f422c..c870c98b 100644 --- a/karavan-app/src/main/webui/src/karavan/shared/ui/RightPanel.css +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/RightPanel.css @@ -5,7 +5,7 @@ flex-direction: column; justify-content: stretch; overflow: hidden; - background-color: var(--pf-t--color--gray--95); + background: var(--pf-t--color--gray--95); } .karavan .right-panel .right-panel-top { height: 64px; /* or whatever fixed height you want */ @@ -21,6 +21,10 @@ border-top-left-radius: var(--pf-t--global--border--radius--300); } +.pf-v6-theme-dark .karavan .right-panel .right-panel-wrapper { + background: var(--pf-t--color--gray--90); +} + .karavan .right-panel .right-panel-wrapper .right-panel-card { height: 100%; display: flex; diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/RightPanel.tsx b/karavan-app/src/main/webui/src/karavan/shared/ui/RightPanel.tsx index d6805fc8..5b8c4157 100644 --- a/karavan-app/src/main/webui/src/karavan/shared/ui/RightPanel.tsx +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/RightPanel.tsx @@ -6,7 +6,7 @@ import {ErrorBoundaryWrapper} from "@shared/ui/ErrorBoundaryWrapper"; interface Props { title: React.ReactNode; toolsStart?: React.ReactNode; - tools: React.ReactNode; + tools?: React.ReactNode; mainPanel: React.ReactNode; } diff --git a/karavan-app/src/main/webui/src/karavan/shared/ui/SideBarFormWrapper.tsx b/karavan-app/src/main/webui/src/karavan/shared/ui/SideBarFormWrapper.tsx new file mode 100644 index 00000000..257610a3 --- /dev/null +++ b/karavan-app/src/main/webui/src/karavan/shared/ui/SideBarFormWrapper.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import {Button, Form} from "@patternfly/react-core"; +import {UseFormReturn} from "react-hook-form"; + +interface AsyncFormWrapperProps { + children: React.ReactNode; + formContext: UseFormReturn<any>; // Accepts any form context + selectedId: string | null; + onSave?: (data: any) => void; + onCancel?: () => void; + onDelete?: () => void; + showDelete?: boolean; + isSubmitDisabled?: boolean; + footer?: React.ReactNode; +} + +export function SideBarFormWrapper({ + children, + formContext, + selectedId, + onSave, + onCancel, + onDelete, + showDelete = false, + isSubmitDisabled = false, + footer + }: AsyncFormWrapperProps) { + + const {formState: {errors}, handleSubmit, getValues} = formContext; + + function onKeyDown(event: React.KeyboardEvent<HTMLFormElement>): void { + event.stopPropagation(); + if (event.key === 'Enter') { + event.preventDefault(); + handleSubmit(onSave)(); + } + } + + // Common Footer Logic + function getFooter() { + const isDisabled = Object.getOwnPropertyNames(errors).length > 0 || isSubmitDisabled; + return ( + <div style={{display: "flex", flexDirection: "row", gap: 8, justifyContent: "space-between", marginTop: 16}}> + <div style={{flexGrow: 1}}> + {showDelete && + <Button + key="delete" + variant={"danger"} + isDanger + isDisabled={isDisabled} + onClick={onDelete} + > + Delete + </Button> + } + </div> + <Button + key="confirm" + variant={"primary"} + isDisabled={isDisabled} + onClick={handleSubmit(onSave)} + > + Save + </Button> + <Button + key="cancel" + variant="link" // Standardized to "link" or "secondary" as you prefer + onClick={onCancel} + > + Cancel + </Button> + </div> + ) + } + + return ( + <Form isHorizontal={true} autoComplete="off" noValidate onKeyDown={onKeyDown}> + <div style={{display: "flex", flexDirection: "column", gap: 16}}> + {children} + </div> + {footer || getFooter()} + </Form> + ); +} \ No newline at end of file
