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 <[email protected]>
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

Reply via email to