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 548a5e41121e50e47ecad193584e310c08d465c3
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Mon Aug 24 17:58:00 2026 -0400

    Karavan-app UI Page System
---
 .../main/webui/src/ui/page-system/SystemPage.css   |  18 ++
 .../main/webui/src/ui/page-system/SystemPage.tsx   | 162 ++++++++++++++++
 .../src/ui/page-system/app-props/AppPropsRow.tsx   |  48 +++++
 .../src/ui/page-system/app-props/AppPropsTable.tsx |  50 +++++
 .../ui/page-system/containers/ContainerPage.css    |  28 +++
 .../page-system/containers/ContainerTableRow.tsx   | 203 +++++++++++++++++++++
 .../ui/page-system/containers/ContainersTable.tsx  |  93 ++++++++++
 .../deployments/DeploymentStatusRow.tsx            |  50 +++++
 .../deployments/DeploymentStatusesTable.tsx        |  68 +++++++
 .../src/ui/page-system/env-vars/EnvVarRow.tsx      |  50 +++++
 .../src/ui/page-system/env-vars/EnvVarsTable.tsx   |  50 +++++
 11 files changed, 820 insertions(+)

diff --git a/karavan-app/src/main/webui/src/ui/page-system/SystemPage.css 
b/karavan-app/src/main/webui/src/ui/page-system/SystemPage.css
new file mode 100644
index 00000000..30c81197
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-system/SystemPage.css
@@ -0,0 +1,18 @@
+.karavan .system-page-toolbar {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    gap: 8px;
+    padding: 8px 16px 8px 16px;
+    position: relative;
+}
+
+.karavan .system-page-toolbar::before {
+    position: absolute;
+    inset: 0;
+    pointer-events: none;
+    content: "";
+    border-bottom: var(--pf-t--global--border--color--default) solid 
var(--pf-t--global--border--width--box--default);
+    border-radius: inherit;
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-system/SystemPage.tsx 
b/karavan-app/src/main/webui/src/ui/page-system/SystemPage.tsx
new file mode 100644
index 00000000..d12b2458
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-system/SystemPage.tsx
@@ -0,0 +1,162 @@
+import React, {useEffect, useState} from 'react';
+import {Button, capitalize, Content, Tab, Tabs, TabTitleText, TextInputGroup, 
TextInputGroupMain, TextInputGroupUtilities, Tooltip} from 
'@patternfly/react-core';
+import {SyncAltIcon, TimesIcon} from "@patternfly/react-icons";
+import {SystemService} from "@services/SystemService";
+import {shallow} from "zustand/shallow";
+import {SystemMenu, SystemMenus, useSystemStore} from "@stores/SystemStore";
+import {RightPanel} from "@shared/ui/RightPanel";
+import {EnvVarsTable} from "./env-vars/EnvVarsTable";
+import {AppPropsTable} from "./app-props/AppPropsTable";
+import {ContainersTable} from "./containers/ContainersTable";
+import {ContainerLogTab} from "@page-project/logs/ContainerLogTab";
+import {useSelectedContainerStore} from "@stores/ProjectStore";
+import {DeploymentStatusesTable} from "./deployments/DeploymentStatusesTable";
+import {KaravanApi} from "@api/KaravanApi";
+import {EventBus} from "@designer/utils/EventBus";
+import {Clean} from "@carbon/icons-react";
+import {useAppConfig} from "@compass/useConfig";
+import './SystemPage.css'
+import {useDataPolling} from "@shared/polling/useDataPolling";
+import {ProjectService} from "@services/ProjectService";
+import {useDeploymentStatusesStore} from "@stores/DeploymentStatusesStore";
+
+export const SystemPage = () => {
+
+    const [filter, setFilter, tabIndex, setTabIndex] = useSystemStore((s) => 
[s.filter, s.setFilter, s.tabIndex, s.setTabIndex], shallow);
+    const [isNewSecretOpen, setIsNewSecretOpen] = useState<boolean>(false);
+    const [isNewConfigMapOpen, setIsNewConfigMapOpen] = 
useState<boolean>(false);
+    const [selectedContainerName, setSelectedContainerName] = 
useSelectedContainerStore((s) => [s.selectedContainerName, 
s.setSelectedContainerName]);
+
+    const fetchDeployments = useDeploymentStatusesStore(s => 
s.fetchDeployments);
+    const isContainerSelected = selectedContainerName !== undefined;
+    const {isDev} = useAppConfig();
+
+    useDataPolling('SystemPageRefresher', refresh, 7000);
+
+    useEffect(() => {
+        refresh();
+        return () => setSelectedContainerName(undefined);
+    }, []);
+
+    useEffect(() => {
+        if (selectedContainerName !== undefined) {
+            setTabIndex('log')
+        }
+    }, [selectedContainerName]);
+
+    function refresh() {
+        SystemService.refresh();
+        ProjectService.refreshAllContainerStatuses();
+        ProjectService.refreshAllCamelContextStatuses();
+        fetchDeployments();
+    }
+
+    function searchInput() {
+        return (
+            <TextInputGroup className="search" style={{width: '300px'}}>
+                <TextInputGroupMain
+                    value={filter}
+                    placeholder='Search by name'
+                    type="text"
+                    autoComplete={"off"}
+                    autoFocus={true}
+                    onChange={(_event, value) => {
+                        setFilter(value);
+                    }}
+                    aria-label="text input example"
+                />
+                <TextInputGroupUtilities>
+                    <Button variant="plain" onClick={_ => {
+                        setFilter('');
+                    }}>
+                        <TimesIcon aria-hidden={true}/>
+                    </Button>
+                </TextInputGroupUtilities>
+            </TextInputGroup>
+        )
+    }
+
+    function tools() {
+        return (
+            <div className="system-page-toolbar" style={{justifyContent: 
"flex-end"}}>
+                <Button icon={<SyncAltIcon/>} variant='link' 
onClick={refresh}/>
+                {searchInput()}
+                {tabIndex === 'secrets' && isDev &&
+                    <Button onClick={event => setIsNewSecretOpen(true)}>Add 
Secret</Button>
+                }
+                {tabIndex === 'configMaps' &&
+                    <Button onClick={event => setIsNewConfigMapOpen(true)}>Add 
ConfigMap</Button>
+                }
+                {tabIndex === 'containers' && isDev &&
+                    <Tooltip content="Cleanup statuses">
+                        <Button className="dev-action-button"
+                                icon={<Clean className="carbon"/>}
+                                isDanger
+                                variant='secondary'
+                                onClick={event => {
+                                    KaravanApi.deleteAllStatuses(res => {
+                                        if (res.status === 200) {
+                                            EventBus.sendAlert('Success', 
'Statuses deleted', "info");
+                                            KaravanApi.restartInformers(res1 
=> {
+                                                if (res1.status === 200) {
+                                                    
EventBus.sendAlert('Success', 'Informers restarted', "info");
+                                                }
+                                            })
+                                        }
+                                    })
+                                }}>
+                        </Button>
+                    </Tooltip>
+                }
+            </div>
+        );
+    }
+
+    function title() {
+        return (<Content component="h2">System</Content>);
+    }
+
+    function getNavigation() {
+        const menu = isContainerSelected ? SystemMenus : SystemMenus.filter(m 
=> m !== 'log');
+        return (
+            <Tabs isNav
+                  activeKey={tabIndex}
+                  onSelect={(_, selectedItem) => {
+                      const menuItem = selectedItem as SystemMenu;
+                      setTabIndex(menuItem);
+                      if (menuItem !== 'log') {
+                          setSelectedContainerName(undefined);
+                      }
+                  }}
+            >
+                {menu.map((item, i) =>
+                    <Tab
+                        key={item}
+                        eventKey={item}
+                        
title={<TabTitleText>{capitalize(item?.toString())}</TabTitleText>}
+                    />
+                )}
+            </Tabs>
+        )
+    }
+
+    return (
+        <RightPanel
+            title={title()}
+            toolsStart={getNavigation()}
+            tools={undefined}
+            mainPanel={
+                <div className="right-panel-card">
+                    <div style={{display: 'flex', flexDirection: 'column', 
height: '100%'}}>
+                        {tabIndex !== 'log' && tools()}
+                        {tabIndex === 'containers' && <ContainersTable/>}
+                        {tabIndex === 'deployments' && 
<DeploymentStatusesTable/>}
+                        {tabIndex === 'envVars' && <EnvVarsTable/>}
+                        {tabIndex === 'appProps' && <AppPropsTable/>}
+                        {tabIndex === 'log' && <ContainerLogTab/>}
+                    </div>
+                </div>
+            }
+        />
+    )
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/app-props/AppPropsRow.tsx 
b/karavan-app/src/main/webui/src/ui/page-system/app-props/AppPropsRow.tsx
new file mode 100644
index 00000000..f2190876
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-system/app-props/AppPropsRow.tsx
@@ -0,0 +1,48 @@
+import React, {useState} from 'react';
+import {Button, TextInput} from '@patternfly/react-core';
+import {Td, Tr} from '@patternfly/react-table';
+import {EyeIcon, EyeSlashIcon} from "@patternfly/react-icons";
+import {SystemApi} from "@api/SystemApi";
+import {Buffer} from 'buffer';
+
+export interface Props {
+    name: string
+}
+
+export function AppPropsRow(props: Props) {
+
+    const [value, setValue] = useState<string>('ASDFGHJKLQWERTYUIOP');
+    const [showValue, setShowValue] = useState<boolean>(false);
+
+    function showValueData() {
+        if (showValue) {
+            setShowValue(false)
+        } else {
+            SystemApi.getAppPropValue(props.name, (val: string) => {
+                setValue(Buffer.from(val, 'base64').toString('binary'));
+                setShowValue(true);
+            });
+        }
+    }
+
+    return (
+        <Tr className='fields-data'>
+            <Td modifier='fitContent'>{props.name}</Td>
+            <Td>
+                <TextInput id={props.name}
+                            autoComplete={'off'}
+                           type={showValue ? 'text' : 'password'}
+                           value={value}
+                           isDisabled
+                />
+            </Td>
+            <Td modifier='fitContent' className='buttons'>
+                <div style={{display: 'flex', flexDirection: 'row', 
justifyContent: 'end'}}>
+                    <Button variant="plain" onClick={event => showValueData()} 
aria-label="Show">
+                        {!showValue ? <EyeIcon/> : <EyeSlashIcon/>}
+                    </Button>
+                </div>
+            </Td>
+        </Tr>
+    )
+}
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/app-props/AppPropsTable.tsx 
b/karavan-app/src/main/webui/src/ui/page-system/app-props/AppPropsTable.tsx
new file mode 100644
index 00000000..913252d0
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-system/app-props/AppPropsTable.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import {Bullseye, EmptyState, Spinner} from '@patternfly/react-core';
+import {InnerScrollContainer, OuterScrollContainer, Table, Tbody, Td, Th, 
Thead, Tr} from '@patternfly/react-table';
+import {shallow} from "zustand/shallow";
+import {AppPropsRow} from "./AppPropsRow";
+import {useSystemStore} from "@stores/SystemStore";
+
+export function AppPropsTable() {
+
+    const [appProps, filter] = useSystemStore((s) => [s.appProps, s.filter], 
shallow);
+
+    function getTableBody() {
+        return (
+            appProps.filter(name => 
name.toLowerCase().includes(filter.toLowerCase())).map((name, index) => (
+                <AppPropsRow key={name} name={name}/>
+            ))
+        )
+    }
+
+    function getTableEmpty() {
+        return (
+            <Tr>
+                <Td colSpan={15}>
+                    <Bullseye>
+                        <EmptyState icon={Spinner}/>
+                    </Bullseye>
+                </Td>
+            </Tr>
+        )
+    }
+
+    return (
+        <OuterScrollContainer>
+            <InnerScrollContainer>
+                <Table variant='compact' borders={false} isStickyHeader>
+                    <Thead>
+                        <Tr>
+                            <Th key='name'>Name</Th>
+                            <Th key='value'>Value</Th>
+                            <Th key='action' screenReaderText='pass'/>
+                        </Tr>
+                    </Thead>
+                    <Tbody>
+                        {appProps && appProps.length > 0 ? getTableBody() : 
getTableEmpty()}
+                    </Tbody>
+                </Table>
+            </InnerScrollContainer>
+        </OuterScrollContainer>
+    )
+}
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/containers/ContainerPage.css 
b/karavan-app/src/main/webui/src/ui/page-system/containers/ContainerPage.css
new file mode 100644
index 00000000..d5534d10
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-system/containers/ContainerPage.css
@@ -0,0 +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.
+ */
+
+.container-page {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+.container-page-section {
+    flex-grow: 1;
+    flex-shrink: 1;
+    overflow: auto;
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/containers/ContainerTableRow.tsx
 
b/karavan-app/src/main/webui/src/ui/page-system/containers/ContainerTableRow.tsx
new file mode 100644
index 00000000..66f62522
--- /dev/null
+++ 
b/karavan-app/src/main/webui/src/ui/page-system/containers/ContainerTableRow.tsx
@@ -0,0 +1,203 @@
+/*
+ * 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, CodeBlock, CodeBlockCode, Content, Flex, FlexItem, 
Label, Modal, ModalBody, ModalFooter, ModalHeader, Spinner, Tooltip} from 
'@patternfly/react-core';
+import '@designer/karavan.css';
+import {Tbody, Td, Tr} from "@patternfly/react-table";
+import {PauseIcon, PlayIcon, StopIcon, TimesIcon} from 
'@patternfly/react-icons';
+import {ContainerStatus} from "@models/ProjectModels";
+import {KaravanApi} from "@api/KaravanApi";
+import {useAppConfigStore} from "@stores/ProjectStore";
+import {ContainerButton} from "@shared/ui/ContainerButton";
+import {useAppConfig} from "@compass/useConfig";
+
+interface Props {
+    index: number
+    container: ContainerStatus
+}
+
+export function ContainerTableRow(props: Props) {
+
+    const config = useAppConfigStore((s) => s.config);
+    const {isKubernetes} = useAppConfig()
+    const [isExpanded, setIsExpanded] = useState<boolean>(false);
+    const [showConfirmation, setShowConfirmation] = useState<boolean>(false);
+    const [command, setCommand] = useState<'run' | 'pause' | 'stop' | 
'delete'>();
+
+    const container = props.container;
+    const commands = container.commands;
+    const ports = container.ports;
+    const isRunning = container.state === 'running';
+    const inTransit = container.inTransit;
+    const color = isRunning ? "green" : "grey";
+    const commandType = 'container'
+
+    function getConfirmation() {
+        return (<Modal
+            variant={"small"}
+            isOpen={showConfirmation}
+            onClose={() => setShowConfirmation(false)}
+            onEscapePress={e => setShowConfirmation(false)}>
+            <ModalHeader>
+                <Content component={'h2'}>Confirmation</Content>
+            </ModalHeader>
+            <ModalBody>
+                <div>{`Confirm ${command} ${commandType} 
${container.containerName}?`}</div>
+            </ModalBody>
+            <ModalFooter>
+                <Button key="confirm" variant={command !== 'run' ? "danger" : 
'primary'} isDanger onClick={e => {
+                    if (command) {
+                        KaravanApi.manageContainer(container.projectId, 
container.type, container.containerName, command, "never", res => {
+                        });
+                        setCommand(undefined);
+                        setShowConfirmation(false);
+                    }
+                }}>Confirm
+                </Button>
+                <Button key="cancel" variant="link"
+                        onClick={e => {
+                            setCommand(undefined);
+                            setShowConfirmation(false);
+                        }}>Cancel</Button>
+            </ModalFooter>
+        </Modal>)
+    }
+
+    const isDevMode = container.type == 'devmode'
+    const isBuild = container.type == 'build'
+
+    return (
+        <Tbody isExpanded={isExpanded}>
+            {showConfirmation && getConfirmation()}
+            <Tr key={container.containerName} style={{verticalAlign: 
"middle"}}>
+                <Td expand={
+                    container.containerName
+                        ? {
+                            rowIndex: props.index,
+                            isExpanded: isExpanded,
+                            onToggle: () => setIsExpanded(!isExpanded),
+                            expandId: 'composable-expandable-example'
+                        }
+                        : undefined}
+                    modifier={"fitContent"} className={'dev-action-button'}>
+                </Td>
+                <Td style={{verticalAlign: "middle"}} modifier={"fitContent"}>
+                    <Badge className="badge">{container.type}</Badge>
+                </Td>
+                {isKubernetes &&
+                    <Td style={{verticalAlign: "middle"}} 
modifier={"fitContent"}>
+                        {!isDevMode && !isBuild && <Label 
color={color}>{container.projectId}</Label>}
+                    </Td>
+                }
+                <Td>
+                    <ContainerButton container={container}/>
+                </Td>
+                <Td modifier={'breakWord'}>
+                    {container.image}
+                </Td>
+                <Td>
+                    <div style={{display: "flex", flexDirection: "column"}}>
+                        {ports !== undefined && ports?.length > 0 && 
ports.sort((a, b) => a.privatePort && b.privatePort && (a.privatePort > 
b.privatePort) ? 1 : -1)
+                            .map((port, index) => {
+                                const start = port.publicPort ? 
port.publicPort + "->" : "";
+                                const end = port.privatePort + "/" + port.type;
+                                return (
+                                    <div key={index} style={{textWrap: 
'nowrap'}}>
+                                        {start + end}
+                                    </div>
+                                )
+                            })}
+                    </div>
+                </Td>
+                <Td>
+                    {isRunning && container.cpuInfo && <Label 
color={color}>{container.cpuInfo}</Label>}
+                </Td>
+                <Td>
+                    {isRunning && container.memoryInfo && <Label 
color={color}>{container.memoryInfo}</Label>}
+                </Td>
+                <Td>
+                    {!inTransit && <Label 
color={color}>{container.state}</Label>}
+                    {inTransit && <Spinner size="lg" aria-label="spinner"/>}
+                </Td>
+                <Td>
+                    {container.type !== 'internal' && container.env === 
config.environment &&
+                        <Flex direction={{default: "row"}} flexWrap={{default: 
"nowrap"}}
+                              spaceItems={{default: 'spaceItemsNone'}}>
+                            <FlexItem>
+                                <Tooltip content={"Start container"} 
position={"bottom"}>
+                                    <Button className="dev-action-button" 
variant={"plain"} icon={<PlayIcon/>}
+                                            
isDisabled={!commands.includes('run') || inTransit}
+                                            onClick={e => {
+                                                setCommand('run');
+                                                setShowConfirmation(true);
+                                            }}></Button>
+                                </Tooltip>
+                            </FlexItem>
+                            <FlexItem>
+                                <Tooltip content={"Pause container"} 
position={"bottom"}>
+                                    <Button className="dev-action-button" 
variant={"plain"} icon={<PauseIcon/>}
+                                            
isDisabled={!commands.includes('pause') || inTransit}
+                                            onClick={e => {
+                                                setCommand('pause');
+                                                setShowConfirmation(true);
+                                            }}></Button>
+                                </Tooltip>
+                            </FlexItem>
+                            <FlexItem>
+                                <Tooltip content={"Stop container"} 
position={"bottom"}>
+                                    <Button className="dev-action-button" 
variant={"plain"} icon={<StopIcon/>}
+                                            
isDisabled={!commands.includes('stop') || inTransit}
+                                            onClick={e => {
+                                                setCommand('stop');
+                                                setShowConfirmation(true);
+                                            }}></Button>
+                                </Tooltip>
+                            </FlexItem>
+                            <FlexItem>
+                                <Tooltip content={"Delete container"} 
position={"bottom"}>
+                                    <Button className="dev-action-button" 
variant={"plain"} icon={<TimesIcon/>}
+                                            
isDisabled={!commands.includes('delete') || inTransit}
+                                            onClick={e => {
+                                                setCommand('delete');
+                                                setShowConfirmation(true);
+                                            }}></Button>
+                                </Tooltip>
+                            </FlexItem>
+                        </Flex>}
+                </Td>
+            </Tr>
+            <Tr isExpanded={isExpanded} style={{verticalAlign: "middle"}}>
+                <Td/>
+                <Td colSpan={9} style={{padding: 8}}>
+                    <div style={{display: 'flex', flexDirection: 'row', 
alignItems: 'center', gap: 8}}>
+                        <Content style={{fontWeight: 'bold'}}>ID</Content>
+                        <Content>{container.containerId}</Content>
+                    </div>
+                </Td>
+            </Tr>
+            <Tr isExpanded={isExpanded} className='fields-data'>
+                <Td/>
+                <Td colSpan={9} modifier={"fitContent"} style={{padding: 0}}>
+                    <CodeBlock style={{borderRadius: 0}}>
+                        <CodeBlockCode 
id="code-content">{JSON.stringify(container, null, 2)}</CodeBlockCode>
+                    </CodeBlock>
+                </Td>
+            </Tr>
+        </Tbody>
+    )
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/containers/ContainersTable.tsx 
b/karavan-app/src/main/webui/src/ui/page-system/containers/ContainersTable.tsx
new file mode 100644
index 00000000..4336c1fc
--- /dev/null
+++ 
b/karavan-app/src/main/webui/src/ui/page-system/containers/ContainersTable.tsx
@@ -0,0 +1,93 @@
+/*
+ * 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 {Bullseye, EmptyState, EmptyStateVariant, Spinner,} from 
'@patternfly/react-core';
+import '@designer/karavan.css';
+import './ContainerPage.css';
+import {ContainerStatus} from "@models/ProjectModels";
+import {InnerScrollContainer, OuterScrollContainer, Table, TableVariant, 
Tbody, Td, Th, Thead, Tr} from '@patternfly/react-table';
+import {SearchIcon} from "@patternfly/react-icons";
+import {shallow} from "zustand/shallow";
+import {ContainerTableRow} from "./ContainerTableRow";
+import {ProjectService} from "@services/ProjectService";
+import {useSystemStore} from "@stores/SystemStore";
+import {useContainerStatusesStore} from "@stores/ContainerStatusesStore";
+import {useAppConfig} from "@compass/useConfig";
+
+export function ContainersTable() {
+
+    const {containers} = useContainerStatusesStore();
+    const [filter, setFilter] = useSystemStore((s) => [s.filter, s.setFilter], 
shallow);
+    const [loading] = useState<boolean>(true);
+    const {isKubernetes} = useAppConfig()
+
+    useEffect(() => {
+        const interval = setInterval(() => {
+            ProjectService.refreshAllContainerStatuses();
+        }, 1000)
+        return () => clearInterval(interval);
+    }, []);
+
+    function getEmptyState() {
+        return (
+            <Tbody>
+                <Tr>
+                    <Td colSpan={8}>
+                        <Bullseye>
+                            {loading && <Spinner className="progress-stepper" 
diameter="80px" aria-label="Loading..."/>}
+                            {!loading &&
+                                <EmptyState headingLevel="h2" 
icon={SearchIcon} titleText="No results found" variant={EmptyStateVariant.sm}>
+                                </EmptyState>
+                            }
+                        </Bullseye>
+                    </Td>
+                </Tr>
+            </Tbody>
+        )
+    }
+
+    const conts = containers.filter(d => 
d.containerName.toLowerCase().includes(filter));
+    return (
+        <OuterScrollContainer>
+            <InnerScrollContainer>
+                <Table aria-label="Projects" variant={TableVariant.compact} 
isStickyHeader>
+                    <Thead>
+                        <Tr>
+                            <Th modifier="fitContent" textCenter={true} 
screenReaderText={'pass'}/>
+                            <Th modifier="fitContent" textCenter={true} 
key='type'>Type</Th>
+                            {isKubernetes &&
+                                <Th key='deployment' 
modifier="fitContent">Deployment</Th>
+                            }
+                            <Th key='container'>Container</Th>
+                            <Th textCenter={true} key='image'>Image</Th>
+                            <Th modifier="fitContent" textCenter={true} 
key='ports'>Ports</Th>
+                            <Th modifier="fitContent" textCenter={true} 
key='cpuInfo'>CPU</Th>
+                            <Th modifier="fitContent" textCenter={true} 
key='memoryInfo'>Memory</Th>
+                            <Th modifier="fitContent" textCenter={true} 
key='state'>State</Th>
+                            <Th modifier="fitContent" textCenter={true} 
key='action'>Actions</Th>
+                        </Tr>
+                    </Thead>
+                    {conts?.map((container: ContainerStatus, index: number) => 
(
+                        <ContainerTableRow 
key={`${container.containerName}-${container.env}`} index={index} 
container={container}/>
+                    ))}
+                    {conts?.length === 0 && getEmptyState()}
+                </Table>
+            </InnerScrollContainer>
+        </OuterScrollContainer>
+    )
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/deployments/DeploymentStatusRow.tsx
 
b/karavan-app/src/main/webui/src/ui/page-system/deployments/DeploymentStatusRow.tsx
new file mode 100644
index 00000000..ec955dee
--- /dev/null
+++ 
b/karavan-app/src/main/webui/src/ui/page-system/deployments/DeploymentStatusRow.tsx
@@ -0,0 +1,50 @@
+import React, {useState} from 'react';
+import {CodeBlock, CodeBlockCode, Label,} from '@patternfly/react-core';
+import {Tbody, Td, Tr} from '@patternfly/react-table';
+import {DeploymentStatus} from "@models/ProjectModels";
+
+export interface Props {
+    index: number
+    deployment: DeploymentStatus
+}
+
+export function DeploymentStatusRow(props: Props) {
+
+    const [isExpanded, setIsExpanded] = useState<boolean>(false);
+
+    const {index, deployment} = props;
+    const isRunning = deployment.readyReplicas === deployment.replicas
+    const color = isRunning ? "green" : "grey";
+    return (
+        <Tbody>
+            <Tr key={deployment.projectId + ':' + index} 
className='camelstatus-data'>
+                <Td noPadding isActionCell expand={{
+                    rowIndex: props.index,
+                    isExpanded: isExpanded,
+                    onToggle: () => setIsExpanded(!isExpanded),
+                    expandId: 'composable-expandable-example'
+                }} modifier={"fitContent"}/>
+                <Td noPadding modifier={"fitContent"}>
+                    {deployment.projectId}
+                </Td>
+                <Td noPadding modifier={"wrap"}>
+                    {deployment.image}
+                </Td>
+                <Td noPadding modifier={"fitContent"}>
+                    <Label color={color}>{deployment.env}</Label>
+                </Td>
+                <Td noPadding modifier={"fitContent"}>
+                    <Label color={color}>{deployment.namespace}</Label>
+                </Td>
+            </Tr>
+            <Tr isExpanded={isExpanded} className='fields-data'>
+                <Td/>
+                <Td colSpan={4} modifier={"fitContent"}>
+                    <CodeBlock>
+                        <CodeBlockCode 
id="code-content">{JSON.stringify(deployment, null, 2)}</CodeBlockCode>
+                    </CodeBlock>
+                </Td>
+            </Tr>
+        </Tbody>
+    )
+}
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/deployments/DeploymentStatusesTable.tsx
 
b/karavan-app/src/main/webui/src/ui/page-system/deployments/DeploymentStatusesTable.tsx
new file mode 100644
index 00000000..18143ca3
--- /dev/null
+++ 
b/karavan-app/src/main/webui/src/ui/page-system/deployments/DeploymentStatusesTable.tsx
@@ -0,0 +1,68 @@
+import React, {useEffect} from 'react';
+import {Bullseye, EmptyState, EmptyStateBody, EmptyStateVariant, Spinner,} 
from '@patternfly/react-core';
+import {InnerScrollContainer, OuterScrollContainer, Table, Tbody, Td, Th, 
Thead, Tr} from '@patternfly/react-table';
+import {useAppConfigStore} from "@stores/ProjectStore";
+import {DeploymentStatusRow} from "./DeploymentStatusRow";
+import {ExclamationCircleIcon} from "@patternfly/react-icons";
+import {useDeploymentStatusesStore} from "@stores/DeploymentStatusesStore";
+
+export function DeploymentStatusesTable() {
+
+    const deployments = useDeploymentStatusesStore((s) => s.deployments);
+    const config = useAppConfigStore((s) => s.config)
+    const isSupported = config.infrastructure === 'kubernetes';
+
+    useEffect(() => {
+    }, []);
+
+    function getTableBody() {
+        return (
+            deployments.map((deployment, index) => (
+                <DeploymentStatusRow key={index} index={index} 
deployment={deployment}/>
+            ))
+        )
+    }
+
+    function getTableEmpty() {
+        return (
+            <Tbody>
+                <Tr>
+                    <Td colSpan={15}>
+                        <Bullseye>
+                            {isSupported
+                                ? <EmptyState icon={Spinner}/>
+                                : <EmptyState titleText="Not supported"
+                                              icon={ExclamationCircleIcon}
+                                              headingLevel="h2"
+                                              variant={EmptyStateVariant.sm}>
+                                    <EmptyStateBody>
+                                        Deployments are only supported in 
<b>Kubernetes</b>
+                                    </EmptyStateBody>
+                                </EmptyState>
+                            }
+                        </Bullseye>
+                    </Td>
+                </Tr>
+            </Tbody>
+        )
+    }
+
+    return (
+        <OuterScrollContainer>
+            <InnerScrollContainer>
+                <Table variant='compact' borders={false} isStickyHeader>
+                    <Thead>
+                        <Tr>
+                            <Th screenReaderText='pass'/>
+                            <Th key='name' modifier={"fitContent"}>Deployment 
Name</Th>
+                            <Th key='id'>Image</Th>
+                            <Th key='type' 
modifier={"fitContent"}>Environment</Th>
+                            <Th key='state' 
modifier={"fitContent"}>Namespace</Th>
+                        </Tr>
+                    </Thead>
+                    {deployments && deployments.length > 0 ? getTableBody() : 
getTableEmpty()}
+                </Table>
+            </InnerScrollContainer>
+        </OuterScrollContainer>
+    )
+}
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/env-vars/EnvVarRow.tsx 
b/karavan-app/src/main/webui/src/ui/page-system/env-vars/EnvVarRow.tsx
new file mode 100644
index 00000000..40fd8773
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-system/env-vars/EnvVarRow.tsx
@@ -0,0 +1,50 @@
+import {Buffer} from "buffer";
+import React, {useState} from 'react';
+import {Button, TextInput} from '@patternfly/react-core';
+import {Td, Tr} from '@patternfly/react-table';
+import {EyeIcon, EyeSlashIcon} from "@patternfly/react-icons";
+import {SystemApi} from "@api/SystemApi";
+
+const DEFAULT_VALUE = "**********************"
+
+export interface Props {
+    name: string
+}
+
+export function EnvVarRow(props: Props) {
+
+    const [value, setValue] = useState<string>(DEFAULT_VALUE);
+    const [showValue, setShowValue] = useState<boolean>(false);
+
+    function showValueData() {
+        if (showValue) {
+            setShowValue(false)
+        } else {
+            SystemApi.getEnvVarValue(props.name, (val: string) => {
+                setValue(Buffer.from(val, 'base64').toString('binary'));
+                setShowValue(true);
+            });
+        }
+    }
+
+    return (
+        <Tr className='fields-data'>
+            <Td modifier='fitContent'>{props.name}</Td>
+            <Td>
+                <TextInput id={props.name}
+                           autoComplete={'off'}
+                           type={showValue ? 'text' : 'password'}
+                           value={value}
+                           isDisabled
+                />
+            </Td>
+            <Td modifier='fitContent' className='buttons'>
+                <div style={{display: 'flex', flexDirection: 'row', 
justifyContent: 'end'}}>
+                    <Button variant="plain" onClick={event => showValueData()} 
aria-label="Show">
+                        {!showValue ? <EyeIcon/> : <EyeSlashIcon/>}
+                    </Button>
+                </div>
+            </Td>
+        </Tr>
+    )
+}
diff --git 
a/karavan-app/src/main/webui/src/ui/page-system/env-vars/EnvVarsTable.tsx 
b/karavan-app/src/main/webui/src/ui/page-system/env-vars/EnvVarsTable.tsx
new file mode 100644
index 00000000..4f095487
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-system/env-vars/EnvVarsTable.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import {Bullseye, EmptyState, Spinner} from '@patternfly/react-core';
+import {InnerScrollContainer, OuterScrollContainer, Table, Tbody, Td, Th, 
Thead, Tr} from '@patternfly/react-table';
+import {shallow} from "zustand/shallow";
+import {EnvVarRow} from "./EnvVarRow";
+import {useSystemStore} from "@stores/SystemStore";
+
+export function EnvVarsTable() {
+
+    const [envVars, filter] = useSystemStore((s) => [s.envVars, s.filter], 
shallow);
+
+    function getTableBody() {
+        return (
+            envVars.filter(name => 
name.toLowerCase().includes(filter.toLowerCase())).map((name, index) => (
+                <EnvVarRow key={name} name={name}/>
+            ))
+        )
+    }
+
+    function getTableEmpty() {
+        return (
+            <Tr>
+                <Td colSpan={15}>
+                    <Bullseye>
+                        <EmptyState icon={Spinner}/>
+                    </Bullseye>
+                </Td>
+            </Tr>
+        )
+    }
+
+    return (
+        <OuterScrollContainer>
+            <InnerScrollContainer>
+                <Table variant='compact' borders={false} isStickyHeader>
+                    <Thead>
+                        <Tr>
+                            <Th key='name'>Name</Th>
+                            <Th key='value'>Value</Th>
+                            <Th key='action' screenReaderText='pass'/>
+                        </Tr>
+                    </Thead>
+                    <Tbody>
+                        {envVars && envVars.length > 0 ? getTableBody() : 
getTableEmpty()}
+                    </Tbody>
+                </Table>
+            </InnerScrollContainer>
+        </OuterScrollContainer>
+    )
+}

Reply via email to