You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2026/08/25 15:19:49 UTC
(camel-karavan) 24/42: Karavan-app UI Page Docs
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 79180ba14ccd47c21aed69338cf8f8858efe8b9a
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Mon Aug 24 17:56:15 2026 -0400
Karavan-app UI Page Docs
---
.../src/ui/page-documentation/Documentation.css | 136 ++++++++++++++
.../ui/page-documentation/DocumentationPage.tsx | 209 +++++++++++++++++++++
.../components/ComponentCard.tsx | 79 ++++++++
.../components/ComponentModal.tsx | 160 ++++++++++++++++
.../components/ComponentsTab.tsx | 46 +++++
.../src/ui/page-documentation/eip/EipCard.tsx | 59 ++++++
.../src/ui/page-documentation/eip/EipModal.tsx | 95 ++++++++++
.../webui/src/ui/page-documentation/eip/EipTab.tsx | 47 +++++
.../ui/page-documentation/useDocumentationHook.tsx | 25 +++
9 files changed, 856 insertions(+)
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/Documentation.css b/karavan-app/src/main/webui/src/ui/page-documentation/Documentation.css
new file mode 100644
index 00000000..cde2d913
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/Documentation.css
@@ -0,0 +1,136 @@
+/*
+ * 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 .documentation-section {
+ height: 100%;
+ overflow: auto;
+
+ .documentation-eip-section {
+ padding: 16px;
+ background-color: transparent;
+ }
+}
+.karavan .documentation-section .documentation-card {
+ cursor: pointer;
+ height: 160px;
+}
+
+.karavan .documentation-section .documentation-card .pf-v6-c-card__header {
+ padding: 8px;
+}
+
+.karavan .documentation-section .documentation-card .pf-v6-c-card__header .custom {
+ margin-left: auto;
+}
+
+.karavan .documentation-section .documentation-card .pf-v6-c-card__header .pf-v6-c-card__header-main {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+}
+
+.karavan .documentation-section .documentation-card .pf-v6-c-card__title {
+ font-size: 15px;
+ font-weight: 400;
+}
+
+.karavan .documentation-section .documentation-card .pf-v6-c-card__body {
+ overflow: hidden;
+ position: relative;
+ line-height: 1.6em;
+ padding-left: 8px;
+}
+
+.documentation-section .documentation-card .icon {
+ height: 24px;
+ max-width: 24px;
+ margin-top: auto;
+ margin-bottom: auto;
+ border: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+
+.pf-v6-theme-dark .documentation-section .documentation-card .icon {
+ background-color: var(--pf-t--color--gray--40);
+ border-color: var(--pf-t--color--gray--40);
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 24px;
+}
+
+.karavan .documentation-section .documentation-card .header-labels {
+ padding: 5px;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+}
+
+.karavan .documentation-section .documentation-card .header-labels .pf-v6-c-card__header-main {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.karavan .documentation-section .documentation-card .footer-labels {
+ padding: 5px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.karavan .documentation-section .documentation-card .version,
+.karavan .documentation-section .documentation-card .support-type,
+.karavan .documentation-section .documentation-card .support-level {
+ white-space: nowrap;
+}
+
+.karavan .documentation-section .documentation-card .labels {
+ opacity: 0.5;
+ font-weight: 200;
+}
+
+.karavan .documentation-section .documentation-card:hover .labels {
+ opacity: 1;
+}
+
+.documentation-section .documentation-card .block-checkbox {
+ align-self: center;
+ margin-left: 6px;
+}
+
+.documentation-section .documentation-card .pf-v6-c-card__body {
+ padding-bottom: 0;
+ height: 54px;
+}
+
+.documentation-section .documentation-card .pf-v6-c-card__body {
+ padding-bottom: 0;
+ height: 54px;
+}
+
+.documentation-section .documentation-card p {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ color: var(--pf-t--global--text--color--subtle);
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/DocumentationPage.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/DocumentationPage.tsx
new file mode 100644
index 00000000..b937a564
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/DocumentationPage.tsx
@@ -0,0 +1,209 @@
+/*
+ * 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, useState} from 'react';
+import {Badge, capitalize, Content, Label, Switch, Tab, Tabs, TabTitleText, TextInput} from "@patternfly/react-core";
+import {EipTab} from "./eip/EipTab";
+import {ComponentsTab} from "./components/ComponentsTab";
+import {ComponentApi} from "@core/api/ComponentApi";
+import {CamelModelMetadata, ElementMeta} from "@core/model/CamelMetadata";
+import {RightPanel} from "@shared/ui/RightPanel";
+import './Documentation.css'
+import {ProjectService} from "@services/ProjectService";
+import {Project, ProjectType} from "@models/ProjectModels";
+import {useFilesStore, useFileStore, useProjectStore} from "@stores/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {extractTitleFromMarkdown} from "@utils/StringUtils";
+import MarkdownPreview from "@uiw/react-markdown-preview";
+import {ErrorBoundaryWrapper} from "@shared/ui/ErrorBoundaryWrapper";
+import {useTheme} from "@compass/theme/ThemeContext";
+import {SourcesTab} from "@page-project/files/SourcesTab";
+import {DeveloperManager} from "@developer/DeveloperManager";
+import {SourcesDrawerPanel} from "@page-project/files/SourcesDrawerPanel";
+import {TabProps} from "@patternfly/react-core/src/components/Tabs/Tab";
+import {useDocumentationHook} from "./useDocumentationHook";
+import {useDocumentationStore} from "@stores/DocumentationStore";
+import {CAMEL_STEP_ELEMENTS} from "@designer/utils/CamelUi";
+
+const BUILD_IN_DOCUMENTATION_PAGES = ['processors', 'components', 'source']
+
+export const DocumentationPage = () => {
+
+ const {isDark} = useTheme();
+ const {loadData} = useDocumentationHook();
+ const [files] = useFilesStore((s) => [s.files], shallow);
+ const [showBlocked, setShowBlocked] = useDocumentationStore((s) => [s.showBlocked, s.setShowBlocked], shallow);
+ const [setProject] = useProjectStore((s) => [s.setProject], shallow);
+ const [file, operation, setFile] = useFileStore((s) => [s.file, s.operation, s.setFile], shallow);
+ const showFilePanel = file !== undefined && operation === 'select';
+ const [pages, setPages] = useState<string[]>(BUILD_IN_DOCUMENTATION_PAGES);
+ const [pageNames, setPageNames] = useState<string[]>(BUILD_IN_DOCUMENTATION_PAGES.map(item => capitalize(item)));
+ const [filter, setFilter] = useState<string>("");
+ const [markdown, setMarkdown] = useState<string>("");
+ const [activeItem, setActiveItem] = useState<string>();
+
+ const onNavSelect = (event: React.MouseEvent<HTMLElement, MouseEvent>, eventKey: TabProps['eventKey']) => {
+ setFile('none', undefined);
+ setActiveItem(eventKey?.toString());
+ }
+
+ useEffect(() => {
+ setProject(new Project({projectId: ProjectType.documentation, name: ProjectType.documentation, type: ProjectType.documentation}), "none");
+ ProjectService.refreshProjectFiles(ProjectType.documentation);
+ loadData();
+ return () => {
+ setProject(new Project(), "none");
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!activeItemIsBuildIn()) {
+ const m = files.filter(f => f.name === activeItem)?.[0]?.code || '';
+ setMarkdown(m)
+ }
+ }, [activeItem]);
+
+ useEffect(() => {
+ const newPages: string[] = [];
+ const newPageNames: string[] = [];
+ files.filter(file => file.name.endsWith(".md")).sort((a, b) => a.name > b.name ? 1 : -1).forEach(file => {
+ newPages.push(file.name);
+ newPageNames.push(extractTitleFromMarkdown(file.code) || file.name);
+ });
+ newPages.push(...BUILD_IN_DOCUMENTATION_PAGES);
+ newPageNames.push(...BUILD_IN_DOCUMENTATION_PAGES.map(item => capitalize(item)));
+ setPages(newPages);
+ setPageNames(newPageNames);
+ if (activeItem === undefined) {
+ setActiveItem(newPages[0])
+ }
+ }, [files]);
+
+ const allComponents = ComponentApi.getComponents();
+ const availableComponents = allComponents?.filter(c => !ComponentApi.getBlockedComponentNames().includes(c.component.name));
+ const components = allComponents
+ .filter(c => {
+ return showBlocked || !ComponentApi.getBlockedComponentNames().includes(c.component.name)
+ })
+ .filter(c => {
+ return c.component?.name?.toLowerCase().includes(filter.toLowerCase())
+ || c.component?.title?.toLowerCase().includes(filter.toLowerCase())
+ || c.component?.description?.toLowerCase().includes(filter.toLowerCase())
+ }).sort((a, b) => (a.component.title?.toLowerCase() > b.component.title?.toLowerCase() ? 1 : -1));
+ const allComponentsQuantity = allComponents?.length;
+ const availableComponentsQuantity = availableComponents?.length;
+
+ const processors = CamelModelMetadata
+ .filter(c => CAMEL_STEP_ELEMENTS.includes(c.className))
+ .filter(c => c.name.toLowerCase().includes(filter.toLowerCase())).sort((a: ElementMeta, b: ElementMeta) => a.name > b.name ? 1 : -1);
+
+ const processorsQuantity = processors?.length;
+
+ function activeItemIsBuildIn() {
+ return activeItem && BUILD_IN_DOCUMENTATION_PAGES.includes(activeItem);
+ }
+
+ function getNavigation() {
+ return (
+ <Tabs onSelect={onNavSelect} isNav activeKey={activeItem}>
+ {pages.map((item, i) => {
+ return (
+ <Tab
+ key={item}
+ eventKey={item}
+ title={<TabTitleText>{pageNames[i]}</TabTitleText>}
+ />
+ )
+ })}
+ </Tabs>
+ )
+ }
+
+ function title() {
+ return <Content>
+ <Content component="h2">Documentation</Content>
+ </Content>
+ }
+
+ function getTools() {
+ return <div>
+ <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6}}>
+ {activeItem === 'components' &&
+ <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6, textWrap: 'nowrap'}}>
+ <Label variant={'outline'}>
+ <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6, textWrap: 'nowrap'}}>
+ <p>Available</p>
+ <Badge>{availableComponentsQuantity}</Badge>
+ <p>from</p>
+ <Badge isRead>{allComponentsQuantity}</Badge>
+ </div>
+ </Label>
+ <Switch
+ label="Show all"
+ isReversed
+ isChecked={showBlocked}
+ onChange={(_event, checked) => setShowBlocked(checked)}
+ />
+ </div>
+ }
+ {activeItem === 'processors' &&
+ <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6, textWrap: 'nowrap'}}>
+ <Label variant={'outline'}>
+ <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6, textWrap: 'nowrap'}}>
+ <p>Available</p>
+ <Badge>{processorsQuantity}</Badge>
+ </div>
+ </Label>
+ </div>
+ }
+ <TextInput className="text-field" type="search" id="search" name="search"
+ value={filter}
+ onChange={(_event, value) => setFilter(value)}
+ autoComplete="off"
+ placeholder="Search by name"/>
+ </div>
+ </div>
+ }
+
+ return (
+ <RightPanel
+ title={title()}
+ toolsStart={getNavigation()}
+ tools={activeItemIsBuildIn() ? getTools() : undefined}
+ drawerPanel={<SourcesDrawerPanel/>}
+ mainPanel={
+ <div className="right-panel-card">
+ <div className="documentation-section">
+ {!showFilePanel && activeItem === 'processors' && <EipTab elements={processors}/>}
+ {!showFilePanel && activeItem === 'components' && <ComponentsTab components={components}/>}
+ {!showFilePanel && activeItem === 'source' && <SourcesTab/>}
+ {showFilePanel && <DeveloperManager/>}
+ {!activeItemIsBuildIn() &&
+ <ErrorBoundaryWrapper onError={error => console.error((error))}>
+ <MarkdownPreview key={"DocumentationMarkdownPreview"}
+ source={markdown}
+ className="documentation-eip-section"
+ wrapperElement={{'data-color-mode': isDark ? 'dark' : 'light'}}
+ />
+ </ErrorBoundaryWrapper>
+ }
+ </div>
+ </div>
+ }
+ />
+ )
+
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentCard.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentCard.tsx
new file mode 100644
index 00000000..5172fee7
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentCard.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 {Badge, Card, CardBody, CardFooter, CardHeader, CardTitle, Checkbox, Content, Flex, Tooltip} from '@patternfly/react-core';
+import {shallow} from "zustand/shallow";
+import {useDocumentationStore} from "@stores/DocumentationStore";
+import {Component} from "@core/model/ComponentModels";
+import {CamelUi} from "@designer/utils/CamelUi";
+import {useDocumentationHook} from "../useDocumentationHook";
+import {ComponentApi} from "@core/api/ComponentApi";
+
+interface Props {
+ component: Component,
+}
+
+export function ComponentCard(props: Props) {
+
+ const {onChangeBlockedList} = useDocumentationHook();
+ const [setComponent, setModalOpen]
+ = useDocumentationStore((s) =>
+ [s.setComponent, s.setModalOpen], shallow)
+ const component = props.component;
+
+ function click(event: React.MouseEvent) {
+ const {target} = event;
+ if (!(target as HTMLElement).parentElement?.className.includes("block-checkbox")) {
+ setComponent(component)
+ setModalOpen(true);
+ }
+ }
+
+ const isBlockedComponent = ComponentApi.getBlockedComponentNames().includes(component.component.name);
+ const isRemote = component.component.remote;
+ const classNameBadge = 'label-component' + (component.component.supportLevel !== 'Stable' ? '-preview' : '')
+ return (
+ <Card isCompact key={component.component.name} className="documentation-card"
+ onClick={event => click(event)}
+ >
+ <CardHeader className="header-labels">
+ <Flex style={{width: '100%'}} gap={{default: 'gapSm'}} justifyContent={{default: 'justifyContentSpaceBetween'}}>
+ <Badge className={classNameBadge}>Component</Badge>
+ <Badge isRead className="support-level labels">{component.component.supportLevel}</Badge>
+ </Flex>
+ <Tooltip content={isBlockedComponent ? 'Unblock' : 'Block'}>
+ <Checkbox id={component.component.name}
+ className="block-checkbox labels"
+ isChecked={!isBlockedComponent}
+ onChange={(_, checked) => onChangeBlockedList("component", component.component.name, checked)}
+ />
+ </Tooltip>
+ </CardHeader>
+ <CardHeader>
+ {CamelUi.getIconForComponent(component.component.title, component.component.label)}
+ <CardTitle>{component.component.title}</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <Content component="p" className="-pf-v6-u-color-200">{component.component.description}</Content>
+ </CardBody>
+ <CardFooter className="footer-labels">
+ <Badge isRead className="labels">{component.component.label}</Badge>
+ <Badge isRead className="labels">{isRemote ? 'remote' : 'internal'}</Badge>
+ </CardFooter>
+ </Card>
+ )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentModal.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentModal.tsx
new file mode 100644
index 00000000..5cad8ee0
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentModal.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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, {useState} from 'react';
+import {Badge, Button, CardHeader, Content, Flex, Modal, ModalBody, ModalFooter, ModalHeader, Tab, Tabs, TabTitleText} from '@patternfly/react-core';
+
+import '@designer/karavan.css';
+import {Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import {CamelUi} from "@designer/utils/CamelUi";
+import {ComponentApi} from "@core/api/ComponentApi";
+import {ComponentHeader, ComponentProperty} from "@core/model/ComponentModels";
+import {useDocumentationStore} from "@stores/DocumentationStore";
+import {shallow} from "zustand/shallow";
+
+export function ComponentModal() {
+
+ const [component, isModalOpen, setModalOpen] = useDocumentationStore((s) =>
+ [s.component, s.isModalOpen, s.setModalOpen], shallow)
+
+ const [tab, setTab] = useState<string | number>('properties');
+
+ const props = new Map<string, ComponentProperty>();
+ if (component) {
+ ComponentApi.getComponentProperties(component?.component.name, "consumer").forEach(cp => props.set(cp.name, cp));
+ ComponentApi.getComponentProperties(component?.component.name, "producer").forEach(cp => props.set(cp.name, cp));
+ }
+
+ const headers = new Map<string, ComponentHeader>();
+ if (component) {
+ ComponentApi.getComponentHeaders(component?.component.name, "consumer").forEach(cp => headers.set(cp.name, cp));
+ ComponentApi.getComponentHeaders(component?.component.name, "producer").forEach(cp => headers.set(cp.name, cp));
+ }
+
+
+ function getPropertiesTable() {
+ return (
+ <Table aria-label="Properties table" variant='compact'>
+ <Thead>
+ <Tr>
+ <Th key='name'>Display Name / Name</Th>
+ <Th key='desc'>Description</Th>
+ <Th key='type'>Type</Th>
+ <Th key='label'>Label</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {Array.from(props.values()).map((p: ComponentProperty, idx: number) => (
+ <Tr key={idx}>
+ <Td key={`${idx}_name`}>
+ <div>
+ <b>{p.displayName}</b>
+ <div>{p.name}</div>
+ </div>
+ </Td>
+ <Td key={`${idx}_desc`}>
+ <div>
+ <div>{p.description}</div>
+ {p.defaultValue && p.defaultValue.toString().length > 0 &&
+ <div>{"Default value: " + p.defaultValue}</div>}
+ </div>
+ </Td>
+ <Td key={`${idx}_type`}>{p.type}</Td>
+ <Td key={`${idx}_label`}>{p.label}</Td>
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ )
+ }
+
+ function getHeadersTable() {
+ return (
+ <Table aria-label="Headers table" variant='compact'>
+ <Thead>
+ <Tr>
+ <Th key='name'>Name</Th>
+ <Th key='desc' modifier={"breakWord"}>Description</Th>
+ <Th key='type'>Group</Th>
+ <Th key='label'>Java Type</Th>
+ <Th key='label'>Default Value</Th>
+ <Th key='label'>Autowired</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {Array.from(headers.values()).map((p: ComponentHeader, idx: number) => (
+ <Tr key={idx}>
+ <Td key={`${idx}_name`}>
+ <div>
+ <b>{p.displayName}</b>
+ <div>{p.name}</div>
+ </div>
+ </Td>
+ <Td key={`${idx}_type`}>{p.description}</Td>
+ <Td key={`${idx}_label`}>{p.group}</Td>
+ <Td key={`${idx}_label`}>{p.javaType}</Td>
+ <Td key={`${idx}_label`}>{p.defaultValue}</Td>
+ <Td key={`${idx}_label`}>{p.autowired}</Td>
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ )
+ }
+
+ const showProps = props.size !== 0;
+ const showHeaders = headers.size !== 0;
+
+ return (
+ <Modal
+ aria-label={"Component"}
+ isOpen={isModalOpen}
+ onClose={() => setModalOpen(false)}
+ >
+ <ModalHeader title={component?.component.title}/>
+ <ModalBody>
+ <Flex direction={{default: 'column'}} key={component?.component.name}
+ className="kamelet-modal-card">
+ <CardHeader actions={{
+ actions: <><Badge className="badge"
+ isRead> {component?.component.label}</Badge></>,
+ hasNoOffset: false,
+ className: undefined
+ }}>
+ {component && CamelUi.getIconForComponent(component.component.title, component.component.label)}
+
+ </CardHeader>
+ <Content component="p" className="description">{component?.component.description}</Content>
+ <Tabs
+ activeKey={tab}
+ onSelect={(event, eventKey) => setTab(eventKey)}
+ aria-label="Tabs in the default example"
+ role="region"
+ >
+ <Tab eventKey={'properties'} title={<TabTitleText>Properties</TabTitleText>}/>
+ <Tab eventKey={'headers'} title={<TabTitleText>Headers</TabTitleText>}/>
+ </Tabs>
+ {tab === 'properties' && showProps && getPropertiesTable()}
+ {tab === 'headers' && showHeaders && getHeadersTable()}
+ </Flex>
+ </ModalBody>
+ <ModalFooter>
+ <Button key="cancel" variant="primary"
+ onClick={e => setModalOpen(false)}>Close</Button>
+ </ModalFooter>
+ </Modal>
+ )
+}
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentsTab.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentsTab.tsx
new file mode 100644
index 00000000..36a8206c
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentsTab.tsx
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import {Gallery} from '@patternfly/react-core';
+import {ComponentCard} from "./ComponentCard";
+import {ComponentModal} from "./ComponentModal";
+import {shallow} from "zustand/shallow";
+import {useDocumentationStore} from "@stores/DocumentationStore";
+import {Component} from '@core/model/ComponentModels';
+
+interface Props {
+ components: Component[],
+}
+
+export function ComponentsTab(props: Props) {
+
+ const [isModalOpen] = useDocumentationStore((s) => [s.isModalOpen], shallow)
+
+ const {components} = props;
+ return (
+ <div className="documentation-eip-section">
+ {isModalOpen && <ComponentModal/>}
+ <div className="kamelets-page">
+ <Gallery hasGutter>
+ {components.map(c => (
+ <ComponentCard key={c.component.name} component={c}/>
+ ))}
+ </Gallery>
+ </div>
+ </div>
+ )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipCard.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipCard.tsx
new file mode 100644
index 00000000..42c3b00a
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipCard.tsx
@@ -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 {Badge, Card, CardBody, CardFooter, CardHeader, CardTitle, Content} from '@patternfly/react-core';
+import {CamelUi} from "@designer/utils/CamelUi";
+import {ElementMeta} from "@core/model/CamelMetadata";
+import {useDocumentationStore} from "@stores/DocumentationStore";
+import {shallow} from "zustand/shallow";
+
+interface Props {
+ element: ElementMeta,
+}
+
+export function EipCard(props: Props) {
+
+ const [setElement, setModalOpen] = useDocumentationStore((s) =>
+ [s.setElement, s.setModalOpen], shallow)
+
+ const element = props.element;
+
+ function click (event: React.MouseEvent) {
+ setElement(element)
+ setModalOpen(true);
+ }
+ return (
+ <Card isCompact key={element.name} className="documentation-card" onClick={event => click(event)}>
+ <CardHeader>
+ <Badge className='label-processor'>Processor</Badge>
+ </CardHeader>
+ <CardHeader>
+ {CamelUi.getIconForDslName(element.className)}
+ <CardTitle>{element.title}</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <Content component="p">{element.description}</Content>
+ </CardBody>
+ <CardFooter className="footer-labels">
+ <div>
+ {element.labels.split(',').map((s: string, i: number) => <Badge key={s + i} isRead
+ className="labels">{s}</Badge>)}
+ </div>
+ </CardFooter>
+ </Card>
+ )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipModal.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipModal.tsx
new file mode 100644
index 00000000..21ab9cdc
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipModal.tsx
@@ -0,0 +1,95 @@
+/*
+ * 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 {Badge, Button, CardHeader, CardTitle, Content, Flex, Modal, ModalBody, ModalFooter, ModalHeader} from '@patternfly/react-core';
+
+import '@designer/karavan.css';
+import {Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import {CamelUi} from "@designer/utils/CamelUi";
+import {PropertyMeta} from "@core/model/CamelMetadata";
+import {useDocumentationStore} from "@stores/DocumentationStore";
+import {shallow} from "zustand/shallow";
+
+
+export function EipModal() {
+
+ const [element, isModalOpen, setModalOpen] = useDocumentationStore((s) =>
+ [s.element, s.isModalOpen, s.setModalOpen], shallow)
+
+ return (
+ <Modal
+ aria-label={"Processor"}
+ isOpen={isModalOpen}
+ onClose={() => setModalOpen(false)}
+ >
+ <ModalHeader title={element?.title}/>
+ <ModalBody>
+ <Flex direction={{default: 'column'}} key={element?.name} className="kamelet-modal-card">
+ <CardHeader actions={{
+ actions: <><Badge className="badge"
+ isRead> {element?.labels}</Badge></>, hasNoOffset: false, className: undefined
+ }}>
+ {element && CamelUi.getIconForDslName(element?.className)}
+
+ </CardHeader>
+ <Content component="p" className="description">{element?.description}</Content>
+ {element?.properties.length !== 0 &&
+ <div>
+ <CardTitle>Properties</CardTitle>
+ <Table aria-label="Simple table" variant='compact'>
+ <Thead>
+ <Tr>
+ <Th key='name' width={10}>Name</Th>
+ <Th key='label'>Label</Th>
+ <Th key='display' width={10}>Display Name</Th>
+ <Th key='desc'>Description</Th>
+ <Th key='type'>Type</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {element?.properties.map((p: PropertyMeta, idx: number) => (
+ <Tr key={idx}>
+ <Td modifier={"fitContent"}>
+ {p.name}
+ </Td>
+ <Td modifier={"fitContent"}>
+ <Badge className="badge" isRead>{p.label}</Badge>
+ </Td>
+ <Td modifier={"fitContent"}>
+ {p.displayName}
+ </Td>
+ <Td key={`${idx}_desc`}>
+ <div>
+ <div>{p.description}</div>
+ {p.defaultValue && p.defaultValue.toString().length > 0 && <div>{"Default value: " + p.defaultValue}</div>}
+ </div>
+ </Td>
+ <Td key={`${idx}_type`}>{p.type}</Td>
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ </div>
+ }
+ </Flex>
+ </ModalBody>
+ <ModalFooter>
+ <Button key="cancel" variant="primary" onClick={e => setModalOpen(false)}>Close</Button>
+ </ModalFooter>
+ </Modal>
+ )
+}
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipTab.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipTab.tsx
new file mode 100644
index 00000000..b76e47aa
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipTab.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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 {Gallery,} from '@patternfly/react-core';
+import {EipCard} from "./EipCard";
+import {EipModal} from "./EipModal";
+import {useDocumentationStore} from "@stores/DocumentationStore";
+import {shallow} from "zustand/shallow";
+import {ElementMeta} from "@core/model/CamelMetadata";
+
+interface Props {
+ elements: ElementMeta[],
+}
+
+export function EipTab(props: Props) {
+
+ const [isModalOpen] = useDocumentationStore((s) => [s.isModalOpen], shallow)
+
+ const { elements } = props;
+
+ return (
+ <div className="documentation-eip-section">
+ {isModalOpen && <EipModal/>}
+ <div className="kamelets-page">
+ <Gallery hasGutter>
+ {elements.map(c => (
+ <EipCard key={c.name} element={c}/>
+ ))}
+ </Gallery>
+ </div>
+ </div>
+ )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/useDocumentationHook.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/useDocumentationHook.tsx
new file mode 100644
index 00000000..57fc4bdd
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-documentation/useDocumentationHook.tsx
@@ -0,0 +1,25 @@
+import {ProjectFile, ProjectType} from "@models/ProjectModels";
+import {ProjectService} from "@services/ProjectService";
+import {ComponentApi} from "@core/api/ComponentApi";
+import {useFilesStore} from "@stores/ProjectStore";
+
+export function useDocumentationHook() {
+
+ const saveFile = useFilesStore((s) => s.saveFile);
+
+ const loadData = async() => {
+ ProjectService.loadCustomKamelets();
+ ProjectService.loadBlockedComponentAndKamelets();
+ }
+
+ async function onChangeBlockedList (type: string, name: string, checked: boolean) {
+ let fileContent = '';
+ if (type === "component") {
+ fileContent = ComponentApi.saveBlockedComponentName(name, checked).join('\n');
+ }
+ const file = new ProjectFile(type + "-blocklist.txt", ProjectType.configuration, fileContent, Date.now());
+ saveFile(file);
+ }
+
+ return { loadData, onChangeBlockedList};
+}
\ No newline at end of file