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
The following commit(s) were added to refs/heads/main by this push: new b6b4fc3 OpenAPi rest generator UI b6b4fc3 is described below commit b6b4fc3e91b36d2f0c71e6ceb6bcf176b4f1c0f8 Author: Marat Gubaidullin <marat.gubaidul...@gmail.com> AuthorDate: Wed Nov 16 17:48:54 2022 -0500 OpenAPi rest generator UI --- karavan-space/src/App.tsx | 30 +++-- karavan-space/src/api/GeneratorApi.tsx | 19 ++++ karavan-space/src/{ => space}/GithubModal.tsx | 24 ++-- karavan-space/src/space/SpaceBus.ts | 41 +++++++ .../{SpaceDesignerPage.tsx => space/SpacePage.tsx} | 34 +++++- karavan-space/src/space/UploadModal.tsx | 125 +++++++++++++++++++++ 6 files changed, 245 insertions(+), 28 deletions(-) diff --git a/karavan-space/src/App.tsx b/karavan-space/src/App.tsx index 1b598d7..9f12428 100644 --- a/karavan-space/src/App.tsx +++ b/karavan-space/src/App.tsx @@ -32,8 +32,11 @@ import EipIcon from "@patternfly/react-icons/dist/js/icons/topology-icon"; import ComponentsIcon from "@patternfly/react-icons/dist/js/icons/module-icon"; import {KaravanIcon} from "./designer/utils/KaravanIcons"; import './designer/karavan.css'; -import {SpaceDesignerPage} from "./SpaceDesignerPage"; -import {GithubModal} from "./GithubModal"; +import {SpacePage} from "./space/SpacePage"; +import {GithubModal} from "./space/GithubModal"; +import {Subscription} from "rxjs"; +import {DslPosition, EventBus} from "./designer/utils/EventBus"; +import {AlertMessage, SpaceBus} from "./space/SpaceBus"; class ToastMessage { id: string = '' @@ -72,6 +75,7 @@ interface State { githubModalIsOpen: boolean, pageId: string, alerts: ToastMessage[], + sub?: Subscription } class App extends React.Component<Props, State> { @@ -96,6 +100,8 @@ class App extends React.Component<Props, State> { } componentDidMount() { + const sub = SpaceBus.onAlert()?.subscribe((evt: AlertMessage) => this.toast(evt.title, evt.message, evt.variant)); + this.setState({sub: sub}); Promise.all([ fetch("kamelets/kamelets.yaml"), fetch("components/components.json") @@ -119,16 +125,17 @@ class App extends React.Component<Props, State> { ); } + componentWillUnmount() { + this.state.sub?.unsubscribe(); + } + save(filename: string, yaml: string, propertyOnly: boolean) { this.setState({name: filename, yaml: yaml}); // console.log(yaml); } - closeGithubModal(close: boolean, toast: boolean, ok: boolean, message: string) { - this.setState({githubModalIsOpen: !close}) - if (toast){ - this.toast(ok ? "Success" : "Error", message, ok?'success' : 'danger'); - } + closeGithubModal() { + this.setState({githubModalIsOpen: false}) } openGithubModal() { @@ -181,7 +188,7 @@ class App extends React.Component<Props, State> { switch (pageId) { case "designer": return ( - <SpaceDesignerPage + <SpacePage name={name} yaml={yaml} onSave={(filename, yaml1, propertyOnly) => this.save(filename, yaml1, propertyOnly)} @@ -204,9 +211,9 @@ class App extends React.Component<Props, State> { } render() { - const {key, loaded, githubModalIsOpen, yaml, name} = this.state; + const {loaded, githubModalIsOpen, yaml, name} = this.state; return ( - <Page key={key} className="karavan"> + <Page className="karavan"> <AlertGroup isToast isLiveRegion> {this.state.alerts.map((e: ToastMessage) => ( <Alert key={e.id} className="main-alert" variant={e.variant} title={e.title} @@ -227,8 +234,7 @@ class App extends React.Component<Props, State> { {loaded !== true && this.getSpinner()} {loaded === true && this.getDesigner()} {loaded === true && githubModalIsOpen && - <GithubModal yaml={yaml} filename={name} isOpen={githubModalIsOpen} - onClose={(close: boolean, t: boolean, ok, message) => this.closeGithubModal(close, t, ok, message)}/>} + <GithubModal yaml={yaml} filename={name} isOpen={githubModalIsOpen} onClose={this.closeGithubModal}/>} </FlexItem> </Flex> </> diff --git a/karavan-space/src/api/GeneratorApi.tsx b/karavan-space/src/api/GeneratorApi.tsx new file mode 100644 index 0000000..61627ff --- /dev/null +++ b/karavan-space/src/api/GeneratorApi.tsx @@ -0,0 +1,19 @@ +export class GeneratorApi { + + static async generate(filename: string, data: string) { + const response = await fetch("https://kameleon.dev/generator/openapi?filename="+ filename, { + method: 'POST', + mode: 'cors', // no-cors, *cors, same-origin + cache: 'no-cache', + credentials: 'same-origin', + headers: { + 'Content-Type': filename.endsWith("json") ? 'application/json' : 'application/yaml' + }, + redirect: 'follow', + referrerPolicy: 'no-referrer', + body: data + }); + return response.text(); + } + +} diff --git a/karavan-space/src/GithubModal.tsx b/karavan-space/src/space/GithubModal.tsx similarity index 91% rename from karavan-space/src/GithubModal.tsx rename to karavan-space/src/space/GithubModal.tsx index 911bbbb..23c80da 100644 --- a/karavan-space/src/GithubModal.tsx +++ b/karavan-space/src/space/GithubModal.tsx @@ -7,18 +7,19 @@ import { Form, TextInputGroupMain, TextInputGroup, Switch, FlexItem, Flex, TextInput } from '@patternfly/react-core'; -import './designer/karavan.css'; -import {GithubApi, GithubParams} from "./api/GithubApi"; +import '../designer/karavan.css'; +import {GithubApi, GithubParams} from "../api/GithubApi"; import GithubImageIcon from "@patternfly/react-icons/dist/esm/icons/github-icon"; -import {StorageApi} from "./api/StorageApi"; -import {KameletApi} from "../../karavan-core/lib/api/KameletApi"; -import {ComponentApi} from "../../karavan-core/lib/api/ComponentApi"; +import {StorageApi} from "../api/StorageApi"; +import {KameletApi} from "../../../karavan-core/lib/api/KameletApi"; +import {ComponentApi} from "../../../karavan-core/lib/api/ComponentApi"; +import {SpaceBus} from "./SpaceBus"; interface Props { yaml: string, filename: string, isOpen: boolean, - onClose: (close: boolean, toast: boolean, ok: boolean, message: string) => void + onClose: () => void } interface State { @@ -77,7 +78,7 @@ export class GithubModal extends React.Component<Props, State> { } }, reason => { - this.props.onClose?.call(this, false, true, false, reason?.toString()); + SpaceBus.sendAlert('Error', reason.toString(), 'danger'); }); } @@ -93,12 +94,12 @@ export class GithubModal extends React.Component<Props, State> { const email: string = (Array.isArray(data[1]) ? Array.from(data[1]).filter(d => d.primary === true)?.at(0)?.email : '') || ''; this.setState({token: token, name: name, email:email, owner: login}) }).catch(err => - this.props.onClose?.call(this, false, true, false, err?.toString()) + SpaceBus.sendAlert('Error', err.toString(), 'danger') ); } closeModal = () => { - this.props.onClose?.call(this, true, false, true, ''); + this.props.onClose?.call(this); } saveAndCloseModal = () => { @@ -122,10 +123,11 @@ export class GithubModal extends React.Component<Props, State> { token, this.props.yaml, result => { this.setState({pushing: false}); - this.props.onClose?.call(this, true, true, true, 'Saved') + SpaceBus.sendAlert('Success', "Saved"); + this.props.onClose?.call(this) }, reason => { - this.props.onClose?.call(this, false, true, false, reason.toString()) + SpaceBus.sendAlert('Error', reason.toString(), 'danger'); this.setState({pushing: false}); } ) diff --git a/karavan-space/src/space/SpaceBus.ts b/karavan-space/src/space/SpaceBus.ts new file mode 100644 index 0000000..2325749 --- /dev/null +++ b/karavan-space/src/space/SpaceBus.ts @@ -0,0 +1,41 @@ +/* + * 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 {Subject} from 'rxjs'; + +const alerts = new Subject<AlertMessage>(); + +export class AlertMessage { + title: string; + message: string; + variant: 'success' | 'danger' | 'warning' | 'info' | 'default'; + + + constructor(title: string, message: string, variant: "success" | "danger" | "warning" | "info" | "default") { + this.title = title; + this.message = message; + this.variant = variant; + } +} + +export const SpaceBus = { + sendAlert: ( + title: string, + message: string, + variant: "success" | "danger" | "warning" | "info" | "default" = 'success' + ) => alerts.next(new AlertMessage(title, message, variant)), + onAlert: () => alerts.asObservable(), +} diff --git a/karavan-space/src/SpaceDesignerPage.tsx b/karavan-space/src/space/SpacePage.tsx similarity index 83% rename from karavan-space/src/SpaceDesignerPage.tsx rename to karavan-space/src/space/SpacePage.tsx index ebdbace..1cb9d7a 100644 --- a/karavan-space/src/SpaceDesignerPage.tsx +++ b/karavan-space/src/space/SpacePage.tsx @@ -21,12 +21,14 @@ import { ToolbarItem, PageSection, TextContent, Text, PageSectionVariants, Flex, FlexItem, Badge, Button, Tooltip, ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'; -import './designer/karavan.css'; +import '../designer/karavan.css'; import DownloadIcon from "@patternfly/react-icons/dist/esm/icons/download-icon"; import DownloadImageIcon from "@patternfly/react-icons/dist/esm/icons/image-icon"; import GithubImageIcon from "@patternfly/react-icons/dist/esm/icons/github-icon"; -import {KaravanDesigner} from "./designer/KaravanDesigner"; +import UploadIcon from "@patternfly/react-icons/dist/esm/icons/upload-icon"; +import {KaravanDesigner} from "../designer/KaravanDesigner"; import Editor from "@monaco-editor/react"; +import {UploadModal} from "./UploadModal"; interface Props { name: string, @@ -37,19 +39,24 @@ interface Props { } interface State { + key: string, karavanDesignerRef: any, + showUploadModal: boolean, mode: "design" | "code", } -export class SpaceDesignerPage extends React.Component<Props, State> { +export class SpacePage extends React.Component<Props, State> { public state: State = { + key: Math.random().toString(), karavanDesignerRef: React.createRef(), mode: "design", + showUploadModal: false } save(filename: string, yaml: string, propertyOnly: boolean) { this.props.onSave?.call(this, filename, yaml, propertyOnly); + this.setState({key: Math.random().toString()}) } download = () => { @@ -72,10 +79,20 @@ export class SpaceDesignerPage extends React.Component<Props, State> { this.props.onPush?.call(this, 'github'); } + openUploadModal = () => { + this.setState({showUploadModal: true}) + } + + addYaml = (yaml: string | undefined) => { + this.setState({showUploadModal: false }); + this.save(this.props.name, this.props.yaml + "\n" + yaml, false); + } + getDesigner = () => { const {name, yaml} = this.props; return ( <KaravanDesigner + key={this.state.key} dark={this.props.dark} ref={this.state.karavanDesignerRef} filename={name} @@ -104,7 +121,7 @@ export class SpaceDesignerPage extends React.Component<Props, State> { render() { - const {mode} = this.state; + const {mode, showUploadModal} = this.state; return ( <PageSection className="kamelet-section designer-page" padding={{default: 'noPadding'}}> <PageSection className="tools-section" padding={{default: 'noPadding'}} @@ -151,6 +168,13 @@ export class SpaceDesignerPage extends React.Component<Props, State> { </Button> </Tooltip> </ToolbarItem> + <ToolbarItem> + <Tooltip content="Upload OpenAPI" position={"bottom"}> + <Button variant="secondary" icon={<UploadIcon/>} onClick={e => this.openUploadModal()}> + OpenAPI + </Button> + </Tooltip> + </ToolbarItem> </ToolbarContent> </Toolbar> </FlexItem> @@ -158,7 +182,7 @@ export class SpaceDesignerPage extends React.Component<Props, State> { </PageSection> {mode === 'design' && this.getDesigner()} {mode === 'code' && this.getEditor()} - + <UploadModal isOpen={showUploadModal} onClose={yaml => this.addYaml(yaml)}/> </PageSection> ); } diff --git a/karavan-space/src/space/UploadModal.tsx b/karavan-space/src/space/UploadModal.tsx new file mode 100644 index 0000000..1177d06 --- /dev/null +++ b/karavan-space/src/space/UploadModal.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { + TextInput, + Button, Modal, FormGroup, ModalVariant, Switch, Form, FileUpload, Radio +} from '@patternfly/react-core'; +import '../designer/karavan.css'; +import {GeneratorApi} from "../api/GeneratorApi"; +import {SpaceBus} from "./SpaceBus"; + +interface Props { + isOpen: boolean, + onClose: (yaml: string | undefined) => void +} + +interface State { + data: string + filename: string + isLoading: boolean + isRejected: boolean + generateRest: boolean + generateRoutes: boolean + generating: boolean +} + +export class UploadModal extends React.Component<Props, State> { + + public state: State = { + data: '', + filename: '', + isLoading: false, + isRejected: false, + generateRest: true, + generateRoutes: true, + generating: false + }; + + closeModal = (yaml: string | undefined) => { + this.props.onClose?.call(this, yaml); + } + + saveAndCloseModal = () => { + this.setState({generating: true}); + const {filename, data} = this.state; + GeneratorApi.generate(filename, data).then(value => { + SpaceBus.sendAlert('Success', 'Generated REST DSL'); + this.setState({generating: false}); + this.closeModal(value); + }).catch(reason => { + SpaceBus.sendAlert('Error', reason.toString(), 'danger'); + this.setState({generating: false}); + }) + } + + handleFileInputChange = (event: React.ChangeEvent<HTMLInputElement> | React.DragEvent<HTMLElement>, file: File) => this.setState({filename: file.name}); + handleFileReadStarted = (fileHandle: File) => this.setState({isLoading: true}); + handleFileReadFinished = (fileHandle: File) => this.setState({isLoading: false}); + handleTextOrDataChange = (data: string) => this.setState({data: data}); + handleFileRejected = (acceptedOrRejected: File[], event: React.DragEvent<HTMLElement>) => this.setState({isRejected: true}); + handleClear = (event: React.MouseEvent<HTMLButtonElement>) => this.setState({ + filename: '', + data: '', + isRejected: false + }); + + + render() { + const {generating} = this.state; + const fileNotUploaded = (this.state.filename === '' || this.state.data === ''); + const isDisabled = fileNotUploaded || generating; + const accept = '.json, .yaml'; + return ( + <Modal + title="Upload OpenAPI" + variant={ModalVariant.small} + isOpen={this.props.isOpen} + onClose={() => this.closeModal(undefined)} + actions={[ + <Button isLoading={generating} key="confirm" variant="primary" onClick={this.saveAndCloseModal} isDisabled={isDisabled}>Save</Button>, + <Button key="cancel" variant="secondary" onClick={event => this.closeModal(undefined)}>Cancel</Button> + ]} + > + <Form> + <FormGroup fieldId="upload"> + <FileUpload + id="file-upload" + value={this.state.data} + filename={this.state.filename} + type="text" + hideDefaultPreview + browseButtonText="Upload" + isLoading={this.state.isLoading} + onFileInputChange={this.handleFileInputChange} + onDataChange={data => this.handleTextOrDataChange(data)} + onTextChange={text => this.handleTextOrDataChange(text)} + onReadStarted={this.handleFileReadStarted} + onReadFinished={this.handleFileReadFinished} + allowEditingUploadedText={false} + onClearClick={this.handleClear} + dropzoneProps={{accept: accept, onDropRejected: this.handleFileRejected}} + validated={this.state.isRejected ? 'error' : 'default'} + /> + </FormGroup> + {/*<FormGroup fieldId="generateRest">*/} + {/* <Switch*/} + {/* id="generate-rest"*/} + {/* label="Generate REST DSL"*/} + {/* labelOff="Do not generate REST DSL"*/} + {/* isChecked={this.state.generateRest}*/} + {/* onChange={checked => this.setState({generateRest: checked})}*/} + {/* />*/} + {/*</FormGroup>*/} + {/*{this.state.generateRest && <FormGroup fieldId="generateRoutes">*/} + {/* <Switch*/} + {/* id="generate-routes"*/} + {/* label="Generate Routes"*/} + {/* labelOff="Do not generate Routes"*/} + {/* isChecked={this.state.generateRoutes}*/} + {/* onChange={checked => this.setState({generateRoutes: checked})}*/} + {/* />*/} + {/*</FormGroup>}*/} + </Form> + </Modal> + ) + } +}; \ No newline at end of file