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> + ) +}
