This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
commit a34cfdee06339365d92540d97214027835017650 Author: Marat Gubaidullin <marat.gubaidul...@gmail.com> AuthorDate: Tue Jun 27 10:14:26 2023 -0400 Refactor for #809 --- karavan-app/src/main/webui/src/Main.tsx | 9 +- .../src/main/webui/src/api/ProjectService.ts | 55 ++++++- karavan-app/src/main/webui/src/api/ProjectStore.ts | 18 +++ .../src/main/webui/src/project/ProjectPage.tsx | 165 +-------------------- .../src/main/webui/src/project/ProjectPanel.tsx | 14 +- .../src/main/webui/src/project/ProjectToolbar.tsx | 80 +++++----- .../main/webui/src/project/PropertiesEditor.tsx | 42 ------ .../src/main/webui/src/project/file/FileEditor.tsx | 143 ++++++++++++++++++ .../src/project/{ => file}/PropertiesTable.tsx | 103 +++++++------ .../src/main/webui/src/project/files/FilesTab.tsx | 19 ++- .../main/webui/src/project/files/FilesToolbar.tsx | 16 ++ .../main/webui/src/project/files/UploadModal.tsx | 16 ++ .../src/project/pipeline/ProjectPipelineTab.tsx | 2 +- .../src/project/{ => pipeline}/ProjectStatus.tsx | 8 +- .../webui/src/project/trace/RunnerInfoTrace.tsx | 16 ++ .../src/project/trace/RunnerInfoTraceModal.tsx | 16 ++ .../src/project/trace/RunnerInfoTraceNode.tsx | 16 ++ .../src/main/webui/src/project/trace/TraceTab.tsx | 16 ++ 18 files changed, 448 insertions(+), 306 deletions(-) diff --git a/karavan-app/src/main/webui/src/Main.tsx b/karavan-app/src/main/webui/src/Main.tsx index 39f3192f..c16599bd 100644 --- a/karavan-app/src/main/webui/src/Main.tsx +++ b/karavan-app/src/main/webui/src/Main.tsx @@ -30,9 +30,9 @@ import {MainLogin} from "./MainLogin"; import {DashboardPage} from "./dashboard/DashboardPage"; import {Subscription} from "rxjs"; import {ProjectEventBus} from "./api/ProjectEventBus"; -import {Project} from "./api/ProjectModels"; +import {Project, ProjectFile} from "./api/ProjectModels"; import {ProjectPage} from "./project/ProjectPage"; -import {useAppConfigStore} from "./api/ProjectStore"; +import {useAppConfigStore, useFileStore} from "./api/ProjectStore"; class ToastMessage { id: string = '' @@ -194,7 +194,10 @@ export class Main extends React.Component<Props, State> { <Tooltip content={page.tooltip} position={"right"}> <Button id={page.pageId} icon={page.icon} variant={"plain"} className={this.state.pageId === page.pageId ? "nav-button-selected" : ""} - onClick={event => this.setState({pageId: page.pageId})} + onClick={event => { + useFileStore.setState({operation:'none', file: undefined}) + this.setState({pageId: page.pageId}); + }} /> </Tooltip> </FlexItem> diff --git a/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-app/src/main/webui/src/api/ProjectService.ts index e346c5bd..feca6ec3 100644 --- a/karavan-app/src/main/webui/src/api/ProjectService.ts +++ b/karavan-app/src/main/webui/src/api/ProjectService.ts @@ -2,10 +2,59 @@ import {KaravanApi} from "./KaravanApi"; import {DeploymentStatus, Project, ProjectFile} from "./ProjectModels"; import {TemplateApi} from "karavan-core/lib/api/TemplateApi"; import {KubernetesAPI} from "../designer/utils/KubernetesAPI"; -import {useDeploymentStatusesStore, useFilesStore, useProjectsStore, useProjectStore} from "./ProjectStore"; +import { unstable_batchedUpdates } from 'react-dom' +import { + useAppConfigStore, + useDeploymentStatusesStore, + useFilesStore, + useFileStore, + useProjectsStore, + useProjectStore +} from "./ProjectStore"; export class ProjectService { + public static pushProject (project: Project, commitMessage: string) { + useProjectStore.setState({isPushing: true}) + const params = { + "projectId": project.projectId, + "message": commitMessage + }; + KaravanApi.push(params, res => { + if (res.status === 200 || res.status === 201) { + useProjectStore.setState({isPushing: false}) + ProjectService.refreshProject(project.projectId); + ProjectService.refreshProjectData(); + } else { + // Todo notification + } + }); + } + + public static saveFile (file: ProjectFile) { + console.log(file) + KaravanApi.postProjectFile(file, res => { + if (res.status === 200) { + const newFile = res.data; + useFileStore.setState({file: newFile}); + unstable_batchedUpdates(() => { + useFilesStore.getState().upsertFile(newFile); + }) + } else { + // console.log(res) //TODO show notification + } + }) + } + + public static refreshProject(projectId: string) { + KaravanApi.getProject(projectId , (project: Project)=> { + useProjectStore.setState({project: project}); + unstable_batchedUpdates(() => { + useProjectsStore.getState().upsertProject(project); + }) + }); + } + public static refreshProjects() { KaravanApi.getProjects((projects: Project[]) => { useProjectsStore.setState({projects: projects}); @@ -61,9 +110,9 @@ export class ProjectService { }); } - - public static refreshProjectData(environment?: string) { + public static refreshProjectData() { const project = useProjectStore.getState().project; + const environment = useAppConfigStore.getState().config.environment; KaravanApi.getProject(project.projectId, (project: Project) => { // ProjectEventBus.selectProject(project); KaravanApi.getTemplatesFiles((files: ProjectFile[]) => { diff --git a/karavan-app/src/main/webui/src/api/ProjectStore.ts b/karavan-app/src/main/webui/src/api/ProjectStore.ts index a524f6bc..4edce361 100644 --- a/karavan-app/src/main/webui/src/api/ProjectStore.ts +++ b/karavan-app/src/main/webui/src/api/ProjectStore.ts @@ -34,6 +34,7 @@ export const useAppConfigStore = create<AppConfigState>((set) => ({ interface ProjectsState { projects: Project[]; setProjects: (projects: Project[]) => void; + upsertProject: (project: Project) => void; } export const useProjectsStore = create<ProjectsState>((set) => ({ @@ -43,10 +44,18 @@ export const useProjectsStore = create<ProjectsState>((set) => ({ projects: ps, }), true); }, + upsertProject: (project: Project) => { + set((state: ProjectsState) => ({ + projects: state.projects.find(f => f.projectId === project.projectId) === undefined + ? [...state.projects, project] + : [...state.projects.filter(f => f.projectId !== project.projectId), project], + }), true); + } })) interface ProjectState { project: Project; + isPushing: boolean, operation: "create" | "select" | "delete" | "none" | "copy"; setProject: (project: Project, operation: "create" | "select" | "delete"| "none" | "copy") => void; } @@ -54,6 +63,7 @@ interface ProjectState { export const useProjectStore = create<ProjectState>((set) => ({ project: new Project(), operation: "none", + isPushing: false, setProject: (p: Project, o: "create" | "select" | "delete"| "none" | "copy") => { set((state: ProjectState) => ({ project: p, @@ -65,6 +75,7 @@ export const useProjectStore = create<ProjectState>((set) => ({ interface FilesState { files: ProjectFile[]; setFiles: (files: ProjectFile[]) => void; + upsertFile: (file: ProjectFile) => void; } export const useFilesStore = create<FilesState>((set) => ({ @@ -74,6 +85,13 @@ export const useFilesStore = create<FilesState>((set) => ({ files: files, }), true); }, + upsertFile: (file: ProjectFile) => { + set((state: FilesState) => ({ + files: state.files.find(f => f.name === file.name) === undefined + ? [...state.files, file] + : [...state.files.filter(f => f.name !== file.name), file], + }), true); + } })) interface FileState { diff --git a/karavan-app/src/main/webui/src/project/ProjectPage.tsx b/karavan-app/src/main/webui/src/project/ProjectPage.tsx index edf0ef6c..d23d86c1 100644 --- a/karavan-app/src/main/webui/src/project/ProjectPage.tsx +++ b/karavan-app/src/main/webui/src/project/ProjectPage.tsx @@ -1,28 +1,20 @@ import React, {useEffect, useState} from 'react'; import { PageSection, - CodeBlockCode, - CodeBlock, Skeleton } from '@patternfly/react-core'; import '../designer/karavan.css'; import {KaravanApi} from "../api/KaravanApi"; import FileSaver from "file-saver"; -import Editor from "@monaco-editor/react"; -import {PropertiesEditor} from "./PropertiesEditor"; -import {ProjectModel, ProjectProperty} from "karavan-core/lib/model/ProjectModel"; -import {ProjectModelApi} from "karavan-core/lib/api/ProjectModelApi"; -import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml"; import {ProjectToolbar} from "./ProjectToolbar"; -import {EventBus} from "../designer/utils/EventBus"; import {ProjectLog} from "./ProjectLog"; -import {AppConfig, ProjectFile, ProjectFileTypes} from "../api/ProjectModels"; -import {useAppConfigStore, useFileStore, useProjectStore} from "../api/ProjectStore"; -import {ProjectService} from "../api/ProjectService"; +import {ProjectFile, ProjectFileTypes} from "../api/ProjectModels"; +import {useFileStore, useProjectStore} from "../api/ProjectStore"; import {MainToolbar} from "../common/MainToolbar"; import {CreateFileModal} from "./CreateFileModal"; import {DeleteFileModal} from "./DeleteFileModal"; import {ProjectTitle} from "./ProjectTitle"; import {ProjectPanel} from "./ProjectPanel"; +import {FileEditor} from "./file/FileEditor"; export const ProjectPage = () => { @@ -31,17 +23,7 @@ export const ProjectPage = () => { const {file, operation} = useFileStore(); const [mode, setMode] = useState<"design" | "code">("design"); const [key, setKey] = useState<string>(''); - const [tab, setTab] = useState<string | number>('files'); const {project} = useProjectStore(); - const {config} = useAppConfigStore(); - - useEffect(() => { - onRefresh(); - }); - - function onRefresh () { - ProjectService.refreshProjectData(config.environment); - } function post (file: ProjectFile) { KaravanApi.postProjectFile(file, res => { @@ -75,133 +57,18 @@ export const ProjectPage = () => { } } - function downloadImage () { - EventBus.sendCommand("downloadImage"); - } - - function addProperty() { - if (file) { - const project = file ? ProjectModelApi.propertiesToProject(file?.code) : ProjectModel.createNew(); - const props = project.properties; - props.push(ProjectProperty.createNew("", "")) - save(file.name, ProjectModelApi.propertiesToString(props)); - setKey(Math.random().toString()); - } - } function tools () { - return <ProjectToolbar key={key} - project={project} + return <ProjectToolbar file={file} mode={mode} - isTemplates={false} - isKamelets={false} - addProperty={() => addProperty()} - downloadImage={() => downloadImage()} editAdvancedProperties={editAdvancedProperties} setEditAdvancedProperties={checked => setEditAdvancedProperties(checked)} setMode={mode => setMode(mode)} setUploadModalOpen={() => setIsUploadModalOpen(isUploadModalOpen)} - needCommit={false} - onRefresh={onRefresh} /> } - // function getDesigner () { - // return ( - // file !== undefined && - // <KaravanDesigner - // dark={false} - // key={"key"} - // filename={file.name} - // yaml={file.code} - // onSave={(name, yaml) => save(name, yaml)} - // onSaveCustomCode={(name, code) => post(new ProjectFile(name + ".java", project.projectId, code, Date.now()))} - // onGetCustomCode={(name, javaType) => { - // return new Promise<string | undefined>(resolve => resolve(files.filter(f => f.name === name + ".java")?.at(0)?.code)) - // }} - // /> - // ) - // } - - function getEditor () { - const language = file?.name.split('.').pop(); - return ( - file !== undefined && - <Editor - height="100vh" - defaultLanguage={language} - theme={'light'} - value={file.code} - className={'code-editor'} - onChange={(value, ev) => { - if (value) { - save(file?.name, value) - } - }} - /> - ) - } - - function deleteEntity (type: 'pod' | 'deployment' | 'pipelinerun', name: string, environment: string) { - switch (type) { - case "deployment": - KaravanApi.deleteDeployment(environment, name, (res: any) => { - if (Array.isArray(res) && Array.from(res).length > 0) - onRefresh(); - }); - break; - case "pod": - KaravanApi.deletePod(environment, name, (res: any) => { - if (Array.isArray(res) && Array.from(res).length > 0) - onRefresh(); - }); - break; - case "pipelinerun": - KaravanApi.stopPipelineRun(environment, name, (res: any) => { - if (Array.isArray(res) && Array.from(res).length > 0) - onRefresh(); - }); - break; - } - } - - function getLogView () { - return ( - <div> - {file !== undefined && file.code.length !== 0 && - <CodeBlock> - <CodeBlockCode id="code-content" className="log-code">{file.code}</CodeBlockCode> - </CodeBlock>} - {(file === undefined || file.code.length === 0) && - <div> - <Skeleton width="25%" screenreaderText="Loading contents"/> - <br/> - <Skeleton width="33%"/> - <br/> - <Skeleton width="50%"/> - <br/> - <Skeleton width="66%"/> - <br/> - <Skeleton width="75%"/> - <br/> - <Skeleton/> - </div>} - </div> - ) - } - - function getPropertiesEditor () { - return ( - file !== undefined && - <PropertiesEditor key={key} - editAdvanced={editAdvancedProperties} - file={file} - onSave={(name, code) => save(name, code)} - /> - ) - } - function isBuildIn(): boolean { return ['kamelets', 'templates'].includes(project.projectId); } @@ -210,28 +77,6 @@ export const ProjectPage = () => { return project.projectId === 'kamelets'; } - function isTemplatesProject(): boolean { - return project.projectId === 'templates'; - } - - function getFilePanel() { - const isYaml = file !== undefined && file.name.endsWith("yaml"); - const isIntegration = isYaml && file?.code && CamelDefinitionYaml.yamlIsIntegration(file.code); - const isProperties = file !== undefined && file.name.endsWith("properties"); - const isLog = file !== undefined && file.name.endsWith("log"); - const isCode = file !== undefined && (file.name.endsWith("java") || file.name.endsWith("groovy") || file.name.endsWith("json")); - const showDesigner = isYaml && isIntegration && mode === 'design'; - const showEditor = isCode || (isYaml && !isIntegration) || (isYaml && mode === 'code'); - return ( - <> - {/*{showDesigner && getDesigner()}*/} - {showEditor && getEditor()} - {isLog && getLogView()} - {isProperties && getPropertiesEditor()} - </> - ) - } - console.log(operation, file) const types = isBuildIn() ? (isKameletsProject() ? ['KAMELET'] : ['CODE', 'PROPERTIES']) : ProjectFileTypes.filter(p => !['PROPERTIES', 'LOG', 'KAMELET'].includes(p.name)).map(p => p.name); @@ -241,7 +86,7 @@ export const ProjectPage = () => { <MainToolbar title={<ProjectTitle/>} tools={tools()}/> </PageSection> {file === undefined && operation !== 'select' && <ProjectPanel/>} - {file !== undefined && operation === 'select' && getFilePanel()} + {file !== undefined && operation === 'select' && <FileEditor/>} <ProjectLog/> <CreateFileModal types={types}/> <DeleteFileModal /> diff --git a/karavan-app/src/main/webui/src/project/ProjectPanel.tsx b/karavan-app/src/main/webui/src/project/ProjectPanel.tsx index 8fa32f2d..fe21736d 100644 --- a/karavan-app/src/main/webui/src/project/ProjectPanel.tsx +++ b/karavan-app/src/main/webui/src/project/ProjectPanel.tsx @@ -1,19 +1,29 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import { Flex, FlexItem, Tabs, Tab } from '@patternfly/react-core'; import '../designer/karavan.css'; import {FilesTab} from "./files/FilesTab"; -import {useProjectStore} from "../api/ProjectStore"; +import {useAppConfigStore, useProjectStore} from "../api/ProjectStore"; import {DashboardTab} from "./dashboard/DashboardTab"; import {TraceTab} from "./trace/TraceTab"; import {ProjectPipelineTab} from "./pipeline/ProjectPipelineTab"; +import {ProjectService} from "../api/ProjectService"; export const ProjectPanel = () => { const [tab, setTab] = useState<string | number>('files'); const {project} = useProjectStore(); + const {config} = useAppConfigStore(); + + useEffect(() => { + onRefresh(); + }); + + function onRefresh () { + ProjectService.refreshProjectData(); + } function isBuildIn(): boolean { return ['kamelets', 'templates'].includes(project.projectId); diff --git a/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx b/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx index 0e7a75c4..8c27e40c 100644 --- a/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx +++ b/karavan-app/src/main/webui/src/project/ProjectToolbar.tsx @@ -32,59 +32,70 @@ import {RunnerToolbar} from "./RunnerToolbar"; import {Project, ProjectFile} from "../api/ProjectModels"; import {ProjectEventBus} from "../api/ProjectEventBus"; import {useAppConfigStore, useFilesStore, useFileStore, useProjectStore} from "../api/ProjectStore"; +import {EventBus} from "../designer/utils/EventBus"; +import {ProjectService} from "../api/ProjectService"; interface Props { - project: Project, - needCommit: boolean, - isTemplates: boolean, - isKamelets: boolean, file?: ProjectFile, mode: "design" | "code", editAdvancedProperties: boolean, - addProperty: () => void, - downloadImage: () => void, setUploadModalOpen: () => void, setEditAdvancedProperties: (checked: boolean) => void, setMode: (mode: "design" | "code") => void, - onRefresh: () => void, } export const ProjectToolbar = (props: Props) => { - const [isPushing, setIsPushing] = useState(false); const [commitMessageIsOpen, setCommitMessageIsOpen] = useState(false); const [commitMessage, setCommitMessage] = useState(''); const [currentRunner, setCurrentRunner] = useState(''); - const [podName, setPodName] = useState(props.project.projectId + '-runner'); const [isJbangRunning, setJbangIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false); const [isDeletingPod, setIsDeletingPod] = useState(false); const [isReloadingPod, setIsReloadingPod] = useState(false); - const {project} = useProjectStore(); + const {project, isPushing} = useProjectStore(); const {files} = useFilesStore(); const {config} = useAppConfigStore(); useEffect(() => { + console.log("ProjectToolbar useEffect", isPushing, project.lastCommitTimestamp) const sub1 = ProjectEventBus.onCurrentRunner()?.subscribe((result) => { setCurrentRunner(result || ''); - setJbangIsRunning(result === props.project.name); + setJbangIsRunning(result === project.name); }); return () => { sub1.unsubscribe(); }; }); + function podName() { + return project.projectId + '-runner'; + } + function needCommit(): boolean { return project ? files.filter(f => f.lastUpdate > project.lastCommitTimestamp).length > 0 : false; } + function downloadImage () { + EventBus.sendCommand("downloadImage"); + } + + function addProperty() { + // if (file) { + // const project = file ? ProjectModelApi.propertiesToProject(file?.code) : ProjectModel.createNew(); + // const props = project.properties; + // props.push(ProjectProperty.createNew("", "")) + // save(file.name, ProjectModelApi.propertiesToString(props)); + // setKey(Math.random().toString()); + // } + } + function jbangRun() { setJbangIsRunning(true); - KaravanApi.runProject(props.project, res => { + KaravanApi.runProject(project, res => { if (res.status === 200 || res.status === 201) { - ProjectEventBus.setCurrentRunner(props.project.name); + ProjectEventBus.setCurrentRunner(project.name); setJbangIsRunning(false); - setPodName(res.data); ProjectEventBus.showLog('container', res.data, config.environment) } else { // Todo notification @@ -96,7 +107,7 @@ export const ProjectToolbar = (props: Props) => { function reloadRunner() { setIsReloadingPod(true); - KaravanApi.getRunnerReload(props.project.projectId, res => { + KaravanApi.getRunnerReload(project.projectId, res => { if (res.status === 200 || res.status === 201) { setIsReloadingPod(false); } else { @@ -109,7 +120,7 @@ export const ProjectToolbar = (props: Props) => { function deleteRunner() { ProjectEventBus.setCurrentRunner(undefined); setIsDeletingPod(true); - KaravanApi.deleteRunner(podName, false, res => { + KaravanApi.deleteRunner(podName(), false, res => { if (res.status === 202) { setIsDeletingPod(false); } else { @@ -120,20 +131,8 @@ export const ProjectToolbar = (props: Props) => { } function push () { - setIsPushing(true); setCommitMessageIsOpen(false); - const params = { - "projectId": props.project.projectId, - "message": commitMessage - }; - KaravanApi.push(params, res => { - if (res.status === 200 || res.status === 201) { - setIsPushing(false); - props.onRefresh(); - } else { - // Todo notification - } - }); + ProjectService.pushProject(project, commitMessage); } function getDate(lastUpdate: number): string { @@ -146,8 +145,7 @@ export const ProjectToolbar = (props: Props) => { } function getLastUpdatePanel() { - const {project, needCommit} = props; - const color = needCommit ? "grey" : "green"; + const color = needCommit() ? "grey" : "green"; const commit = project?.lastCommit; return ( <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexStart"}}> @@ -169,7 +167,7 @@ export const ProjectToolbar = (props: Props) => { } function getTemplatesToolbar() { - const {file,needCommit, editAdvancedProperties, setUploadModalOpen} = props; + const {file, editAdvancedProperties, setUploadModalOpen} = props; const isFile = file !== undefined; const isProperties = file !== undefined && file.name.endsWith("properties"); return <Toolbar id="toolbar-group-types"> @@ -183,7 +181,7 @@ export const ProjectToolbar = (props: Props) => { <Tooltip content="Commit and push to git" position={"bottom"}> <Button isLoading={isPushing ? true : undefined} isSmall - variant={needCommit ? "primary" : "secondary"} + variant={needCommit() ? "primary" : "secondary"} className="project-button" icon={!isPushing ? <PushIcon/> : <div></div>} onClick={() => setCommitMessageIsOpen(true)}> @@ -215,8 +213,8 @@ export const ProjectToolbar = (props: Props) => { } function getProjectToolbar() { - const {file,needCommit, mode, editAdvancedProperties, project, - addProperty, setEditAdvancedProperties, downloadImage, setUploadModalOpen} = props; + const {file, mode, editAdvancedProperties, + setEditAdvancedProperties, setUploadModalOpen} = props; const isFile = file !== undefined; const isYaml = file !== undefined && file.name.endsWith("yaml"); const isIntegration = isYaml && file?.code && CamelDefinitionYaml.yamlIsIntegration(file.code); @@ -231,7 +229,7 @@ export const ProjectToolbar = (props: Props) => { <Tooltip content="Commit and push to git" position={"bottom-end"}> <Button isLoading={isPushing ? true : undefined} isSmall - variant={needCommit ? "primary" : "secondary"} + variant={needCommit() ? "primary" : "secondary"} className="project-button" icon={!isPushing ? <PushIcon/> : <div></div>} onClick={() => { @@ -299,7 +297,15 @@ export const ProjectToolbar = (props: Props) => { ) } - const {isTemplates} = props; + function isKameletsProject(): boolean { + return project.projectId === 'kamelets'; + } + + function isTemplatesProject(): boolean { + return project.projectId === 'templates'; + } + + const isTemplates = isTemplatesProject(); return ( <> {isTemplates && getTemplatesToolbar()} diff --git a/karavan-app/src/main/webui/src/project/PropertiesEditor.tsx b/karavan-app/src/main/webui/src/project/PropertiesEditor.tsx deleted file mode 100644 index bf2f4609..00000000 --- a/karavan-app/src/main/webui/src/project/PropertiesEditor.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { - PageSection, -} from '@patternfly/react-core'; -import '../designer/karavan.css'; -import {PropertiesTable} from "./PropertiesTable"; -import {ProjectModel} from "karavan-core/lib/model/ProjectModel"; -import {ProjectModelApi} from "karavan-core/lib/api/ProjectModelApi"; -import {ProjectFile} from "../api/ProjectModels"; - -interface Props { - file: ProjectFile, - editAdvanced: boolean, - onSave?: (filename: string, code: string) => void -} - -interface State { - project: ProjectModel, - file: ProjectFile, -} - -export class PropertiesEditor extends React.Component<Props, State> { - - public state: State = { - project: this.props.file ? ProjectModelApi.propertiesToProject(this.props.file?.code) : ProjectModel.createNew(), - file: this.props.file, - } - - render() { - const file = this.state.file; - const project = file ? ProjectModelApi.propertiesToProject(file?.code) : ProjectModel.createNew(); - return ( - <PageSection isFilled className="kamelets-page" padding={{default: file !== undefined ? 'noPadding' : 'padding'}}> - <PropertiesTable - editAdvanced={this.props.editAdvanced} - properties={project.properties} - onChange={properties => this.props.onSave?.call(this, file.name, ProjectModelApi.propertiesToString(properties))} - /> - </PageSection> - ) - } -} diff --git a/karavan-app/src/main/webui/src/project/file/FileEditor.tsx b/karavan-app/src/main/webui/src/project/file/FileEditor.tsx new file mode 100644 index 00000000..c58008e2 --- /dev/null +++ b/karavan-app/src/main/webui/src/project/file/FileEditor.tsx @@ -0,0 +1,143 @@ +/* + * 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 { + CodeBlockCode, + CodeBlock, Skeleton +} from '@patternfly/react-core'; +import '../../designer/karavan.css'; +import Editor from "@monaco-editor/react"; +import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml"; +import {ProjectFile, ProjectFileTypes} from "../../api/ProjectModels"; +import {useAppConfigStore, useFilesStore, useFileStore, useProjectStore} from "../../api/ProjectStore"; +import {KaravanDesigner} from "../../designer/KaravanDesigner"; +import {ProjectService} from "../../api/ProjectService"; +import {PropertiesTable} from "./PropertiesTable"; + +export const FileEditor = () => { + + const [editAdvancedProperties] = useState<boolean>(false); + const {file, operation} = useFileStore(); + const [mode, setMode] = useState<"design" | "code">("design"); + const [key, setKey] = useState<string>(''); + const {project} = useProjectStore(); + const {config} = useAppConfigStore(); + + function save (name: string, code: string) { + if (file) { + file.code = code; + ProjectService.saveFile(file); + } + } + + function onGetCustomCode (name: string, javaType: string): Promise<string | undefined> { + const files = useFilesStore.getState().files; + return new Promise<string | undefined>(resolve => resolve(files.filter(f => f.name === name + ".java")?.at(0)?.code)); + } + + function getDesigner () { + return ( + file !== undefined && + <KaravanDesigner + dark={false} + key={"key"} + filename={file.name} + yaml={file.code} + onSave={(name, yaml) => save(name, yaml)} + onSaveCustomCode={(name, code) => + ProjectService.saveFile(new ProjectFile(name + ".java", project.projectId, code, Date.now()))} + onGetCustomCode={onGetCustomCode} + /> + ) + } + + function getEditor () { + const language = file?.name.split('.').pop(); + return ( + file !== undefined && + <Editor + height="100vh" + defaultLanguage={language} + theme={'light'} + value={file.code} + className={'code-editor'} + onChange={(value, ev) => { + if (value) { + save(file?.name, value) + } + }} + /> + ) + } + + function getLogView () { + return ( + <div> + {file !== undefined && file.code.length !== 0 && + <CodeBlock> + <CodeBlockCode id="code-content" className="log-code">{file.code}</CodeBlockCode> + </CodeBlock>} + {(file === undefined || file.code.length === 0) && + <div> + <Skeleton width="25%" screenreaderText="Loading contents"/> + <br/> + <Skeleton width="33%"/> + <br/> + <Skeleton width="50%"/> + <br/> + <Skeleton width="66%"/> + <br/> + <Skeleton width="75%"/> + <br/> + <Skeleton/> + </div>} + </div> + ) + } + + + function isBuildIn(): boolean { + return ['kamelets', 'templates'].includes(project.projectId); + } + + function isKameletsProject(): boolean { + return project.projectId === 'kamelets'; + } + + function isTemplatesProject(): boolean { + return project.projectId === 'templates'; + } + + const types = isBuildIn() + ? (isKameletsProject() ? ['KAMELET'] : ['CODE', 'PROPERTIES']) + : ProjectFileTypes.filter(p => !['PROPERTIES', 'LOG', 'KAMELET'].includes(p.name)).map(p => p.name); + const isYaml = file !== undefined && file.name.endsWith("yaml"); + const isIntegration = isYaml && file?.code && CamelDefinitionYaml.yamlIsIntegration(file.code); + const isProperties = file !== undefined && file.name.endsWith("properties"); + const isLog = file !== undefined && file.name.endsWith("log"); + const isCode = file !== undefined && (file.name.endsWith("java") || file.name.endsWith("groovy") || file.name.endsWith("json")); + const showDesigner = isYaml && isIntegration && mode === 'design'; + const showEditor = isCode || (isYaml && !isIntegration) || (isYaml && mode === 'code'); + return ( + <> + {showDesigner && getDesigner()} + {showEditor && getEditor()} + {isLog && getLogView()} + {isProperties && file !== undefined && <PropertiesTable/>} + </> + ) +} diff --git a/karavan-app/src/main/webui/src/project/PropertiesTable.tsx b/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx similarity index 53% rename from karavan-app/src/main/webui/src/project/PropertiesTable.tsx rename to karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx index 72287a35..5b58cfeb 100644 --- a/karavan-app/src/main/webui/src/project/PropertiesTable.tsx +++ b/karavan-app/src/main/webui/src/project/file/PropertiesTable.tsx @@ -14,85 +14,90 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, {useState} from 'react'; import { Button, Modal, PageSection, TextInput } from '@patternfly/react-core'; -import '../designer/karavan.css'; +import '../../designer/karavan.css'; import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon"; -import {ProjectProperty} from "karavan-core/lib/model/ProjectModel"; +import {ProjectModel, ProjectProperty} from "karavan-core/lib/model/ProjectModel"; +import {useFileStore} from "../../api/ProjectStore"; +import {ProjectService} from "../../api/ProjectService"; +import {ProjectModelApi} from "karavan-core/lib/api/ProjectModelApi"; -interface Props { - properties: ProjectProperty[] - onChange?: (properties: ProjectProperty[]) => void - editAdvanced: boolean -} +export const PropertiesTable = () => { -interface State { - properties: ProjectProperty[] - showDeleteConfirmation: boolean - deleteId?: string -} + const {file, operation} = useFileStore(); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false); + const [editAdvanced, setEditAdvanced] = useState<boolean>(false); + const [deleteId, setDeleteId] = useState<string | undefined>(undefined); + const [properties, setProperties] = useState<ProjectProperty[]>([]); -export class PropertiesTable extends React.Component<Props, State> { - - public state: State = { - properties: this.props.properties, - showDeleteConfirmation: false, + function save (props: ProjectProperty[]) { + console.log("save") + if (file) { + file.code = ProjectModelApi.propertiesToString(props); + console.log("save", file) + ProjectService.saveFile(file); + } } - sendUpdate = (props: ProjectProperty[]) => { - this.props.onChange?.call(this, props); + function getProjectModel (): ProjectModel { + return file ? ProjectModelApi.propertiesToProject(file?.code) : ProjectModel.createNew() } - changeProperty(p: ProjectProperty, field: "key" | "value", val?: string) { + function changeProperty(p: ProjectProperty, field: "key" | "value", val?: string) { const key: string = field === 'key' && val !== undefined ? val : p.key; const value: any = field === 'value' ? val : p.value; const property: ProjectProperty = {id: p.id, key: key, value: value}; - const props = this.state.properties.map(prop => prop.id === property.id ? property : prop); - this.setState({properties: props}); - this.sendUpdate(props); + const properties = getProjectModel().properties; + const props = properties.map(prop => prop.id === property.id ? property : prop); + save(props); } - startDelete(id: string) { - this.setState({showDeleteConfirmation: true, deleteId: id}); + function startDelete(id: string) { + console.log("startDelete", id) + setShowDeleteConfirmation(true); + setDeleteId(id); } - confirmDelete() { - const props = this.state.properties.filter(p => p.id !== this.state.deleteId); - this.setState({properties: props, showDeleteConfirmation: false, deleteId: undefined}); - this.sendUpdate(props); + function confirmDelete() { + console.log("confirmDelete") + const properties = getProjectModel().properties; + const props = properties.filter(p => p.id !== deleteId); + save(props); + setShowDeleteConfirmation(false); + setDeleteId(undefined); } - getDeleteConfirmation() { + function getDeleteConfirmation() { return (<Modal className="modal-delete" title="Confirmation" - isOpen={this.state.showDeleteConfirmation} - onClose={() => this.setState({showDeleteConfirmation: false})} + isOpen={showDeleteConfirmation} + onClose={() => setShowDeleteConfirmation(false)} actions={[ - <Button key="confirm" variant="primary" onClick={e => this.confirmDelete()}>Delete</Button>, + <Button key="confirm" variant="primary" onClick={e => confirmDelete()}>Delete</Button>, <Button key="cancel" variant="link" - onClick={e => this.setState({showDeleteConfirmation: false})}>Cancel</Button> + onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button> ]} - onEscapePress={e => this.setState({showDeleteConfirmation: false})}> + onEscapePress={e => setShowDeleteConfirmation(false)}> <div>Delete property?</div> </Modal>) } - getTextInputField(property: ProjectProperty, field: "key" | "value", readOnly: boolean) { - return (<TextInput isDisabled={readOnly} isRequired={true} className="text-field" type={"text"} id={"key"} name={"key"} + function getTextInputField(property: ProjectProperty, field: "key" | "value", readOnly: boolean) { + return (<TextInput isDisabled={readOnly} isRequired={true} className="text-field" type={"text"} id={field + "-" + property.key} value={field === "key" ? property.key : property.value} - onChange={val => this.changeProperty(property, field, val)}/>) + onChange={val => changeProperty(property, field, val)}/>) } - render() { - const properties = this.state.properties; - return ( + return ( + <PageSection isFilled className="kamelets-page" padding={{default: file !== undefined ? 'noPadding' : 'padding'}}> <PageSection padding={{default: "noPadding"}}> {properties.length > 0 && <TableComposable aria-label="Property table" variant='compact' borders={false} @@ -106,21 +111,21 @@ export class PropertiesTable extends React.Component<Props, State> { </Thead> <Tbody> {properties.map((property, idx: number) => { - const readOnly = (property.key.startsWith("camel.jbang") || property.key.startsWith("camel.karavan")) && !this.props.editAdvanced; + const readOnly = (property.key.startsWith("camel.jbang") || property.key.startsWith("camel.karavan")) && !editAdvanced; return ( <Tr key={property.id}> - <Td noPadding width={10} dataLabel="key">{this.getTextInputField(property, "key", readOnly)}</Td> - <Td noPadding width={20} dataLabel="value">{this.getTextInputField(property, "value", readOnly)}</Td> + <Td noPadding width={10} dataLabel="key">{getTextInputField(property, "key", readOnly)}</Td> + <Td noPadding width={20} dataLabel="value">{getTextInputField(property, "value", readOnly)}</Td> <Td noPadding isActionCell dataLabel="delete" className="delete-cell"> {!readOnly && <Button variant={"plain"} icon={<DeleteIcon/>} className={"delete-button"} - onClick={event => this.startDelete(property.id)}/>} + onClick={event => startDelete(property.id)}/>} </Td> </Tr> )})} </Tbody> </TableComposable>} - {this.state.showDeleteConfirmation && this.getDeleteConfirmation()} + {showDeleteConfirmation && getDeleteConfirmation()} </PageSection> - ) - } + </PageSection> + ) } \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/project/files/FilesTab.tsx b/karavan-app/src/main/webui/src/project/files/FilesTab.tsx index 3ae79da5..28e25c10 100644 --- a/karavan-app/src/main/webui/src/project/files/FilesTab.tsx +++ b/karavan-app/src/main/webui/src/project/files/FilesTab.tsx @@ -6,32 +6,36 @@ import { EmptyState, EmptyStateVariant, EmptyStateIcon, - Title, PageSection, PanelHeader, Panel, Tooltip, + Title, PageSection, PanelHeader, Panel, Tooltip, Label, } from '@patternfly/react-core'; import '../../designer/karavan.css'; import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon"; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; -import {useFilesStore, useFileStore} from "../../api/ProjectStore"; +import {useFilesStore, useFileStore, useProjectStore} from "../../api/ProjectStore"; import {getProjectFileType, ProjectFile} from "../../api/ProjectModels"; import {FileToolbar} from "./FilesToolbar"; import DownloadIcon from "@patternfly/react-icons/dist/esm/icons/download-icon"; import FileSaver from "file-saver"; - export const FilesTab = () => { const {files} = useFilesStore(); + const {project} = useProjectStore(); function getDate(lastUpdate: number): string { if (lastUpdate) { const date = new Date(lastUpdate); - return date.toDateString() + ' ' + date.toLocaleTimeString(); + return date.toISOString().slice(0, 19).replace('T',' '); } else { return "N/A" } } + function needCommit(lastUpdate: number): boolean { + return lastUpdate > project.lastCommitTimestamp; + } + function download (file: ProjectFile) { if (file) { const type = file.name.endsWith("yaml") ? "application/yaml;charset=utf-8" : undefined; @@ -72,7 +76,12 @@ export const FilesTab = () => { </Button> </Td> <Td> - {getDate(file.lastUpdate)} + {needCommit(file.lastUpdate) && + <Tooltip content="Updated after last commit" position={"right"}> + <Label color="grey">{getDate(file.lastUpdate)}</Label> + </Tooltip> + } + {!needCommit(file.lastUpdate) && getDate(file.lastUpdate)} </Td> <Td modifier={"fitContent"}> {file.projectId !== 'templates' && diff --git a/karavan-app/src/main/webui/src/project/files/FilesToolbar.tsx b/karavan-app/src/main/webui/src/project/files/FilesToolbar.tsx index 2ef4c092..00835fe4 100644 --- a/karavan-app/src/main/webui/src/project/files/FilesToolbar.tsx +++ b/karavan-app/src/main/webui/src/project/files/FilesToolbar.tsx @@ -1,3 +1,19 @@ +/* + * 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 { Button, diff --git a/karavan-app/src/main/webui/src/project/files/UploadModal.tsx b/karavan-app/src/main/webui/src/project/files/UploadModal.tsx index a2af3243..d0d73e65 100644 --- a/karavan-app/src/main/webui/src/project/files/UploadModal.tsx +++ b/karavan-app/src/main/webui/src/project/files/UploadModal.tsx @@ -1,3 +1,19 @@ +/* + * 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 { TextInput, diff --git a/karavan-app/src/main/webui/src/project/pipeline/ProjectPipelineTab.tsx b/karavan-app/src/main/webui/src/project/pipeline/ProjectPipelineTab.tsx index 6cd7e4fd..e2209f89 100644 --- a/karavan-app/src/main/webui/src/project/pipeline/ProjectPipelineTab.tsx +++ b/karavan-app/src/main/webui/src/project/pipeline/ProjectPipelineTab.tsx @@ -1,6 +1,6 @@ import React from 'react'; import '../../designer/karavan.css'; -import {ProjectStatus} from "../ProjectStatus"; +import {ProjectStatus} from "./ProjectStatus"; import {PageSection} from "@patternfly/react-core"; import {useAppConfigStore, useProjectStore} from "../../api/ProjectStore"; diff --git a/karavan-app/src/main/webui/src/project/ProjectStatus.tsx b/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx similarity index 98% rename from karavan-app/src/main/webui/src/project/ProjectStatus.tsx rename to karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx index 3e57ebe9..27c898dc 100644 --- a/karavan-app/src/main/webui/src/project/ProjectStatus.tsx +++ b/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx @@ -6,16 +6,16 @@ import { DescriptionListGroup, DescriptionListDescription, Spinner, Tooltip, Flex, FlexItem, LabelGroup, Label, Modal, Badge, CardBody, Card } from '@patternfly/react-core'; -import '../designer/karavan.css'; -import {KaravanApi} from "../api/KaravanApi"; +import '../../designer/karavan.css'; +import {KaravanApi} from "../../api/KaravanApi"; import BuildIcon from "@patternfly/react-icons/dist/esm/icons/build-icon"; import RolloutIcon from "@patternfly/react-icons/dist/esm/icons/process-automation-icon"; import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon"; import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon"; import ClockIcon from "@patternfly/react-icons/dist/esm/icons/clock-icon"; import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon"; -import {CamelStatus, DeploymentStatus, PipelineStatus, PodStatus, Project} from "../api/ProjectModels"; -import {ProjectEventBus} from "../api/ProjectEventBus"; +import {CamelStatus, DeploymentStatus, PipelineStatus, PodStatus, Project} from "../../api/ProjectModels"; +import {ProjectEventBus} from "../../api/ProjectEventBus"; interface Props { project: Project, diff --git a/karavan-app/src/main/webui/src/project/trace/RunnerInfoTrace.tsx b/karavan-app/src/main/webui/src/project/trace/RunnerInfoTrace.tsx index 8f0f655d..cf2a37ce 100644 --- a/karavan-app/src/main/webui/src/project/trace/RunnerInfoTrace.tsx +++ b/karavan-app/src/main/webui/src/project/trace/RunnerInfoTrace.tsx @@ -1,3 +1,19 @@ +/* + * 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 { Bullseye, diff --git a/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceModal.tsx b/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceModal.tsx index 4f83cedc..6940f8a8 100644 --- a/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceModal.tsx +++ b/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceModal.tsx @@ -1,3 +1,19 @@ +/* + * 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 { Flex, FlexItem, diff --git a/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceNode.tsx b/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceNode.tsx index 7dbb4c45..975d7df5 100644 --- a/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceNode.tsx +++ b/karavan-app/src/main/webui/src/project/trace/RunnerInfoTraceNode.tsx @@ -1,3 +1,19 @@ +/* + * 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 { CodeBlock, CodeBlockCode, DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow, DataListWrapModifier, diff --git a/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx b/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx index 31f1c0c9..1c108f8b 100644 --- a/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx +++ b/karavan-app/src/main/webui/src/project/trace/TraceTab.tsx @@ -1,3 +1,19 @@ +/* + * 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 { PageSection } from '@patternfly/react-core';