diff --git a/app/ui-react/packages/api/src/callFetch.ts b/app/ui-react/packages/api/src/callFetch.ts index b3334797d19..4f8ba4f8ffc 100644 --- a/app/ui-react/packages/api/src/callFetch.ts +++ b/app/ui-react/packages/api/src/callFetch.ts @@ -56,6 +56,7 @@ export function callFetch({ includeReferrerPolicy = true, stringifyBody = true, }: IFetch) { + headers = { ...headers }; if (includeAccept && !(acceptId in headers)) { headers[acceptId] = accept; } @@ -71,6 +72,7 @@ export function callFetch({ stringifyBody ? JSON.stringify(body) : body; + return fetch(url, { body: data, cache: 'no-cache', diff --git a/app/ui-react/packages/api/src/helpers/integrationFunctions.ts b/app/ui-react/packages/api/src/helpers/integrationFunctions.ts index f1a803c870c..f7f72ab6baa 100644 --- a/app/ui-react/packages/api/src/helpers/integrationFunctions.ts +++ b/app/ui-react/packages/api/src/helpers/integrationFunctions.ts @@ -16,7 +16,12 @@ import { } from '@syndesis/models'; import { key } from '@syndesis/utils'; import produce from 'immer'; -import { AGGREGATE, DataShapeKinds, ENDPOINT } from '../constants'; +import { + AGGREGATE, + API_PROVIDER, + DataShapeKinds, + ENDPOINT, +} from '../constants'; import { getConnectionIcon } from './connectionFunctions'; export const NEW_INTEGRATION = { @@ -80,10 +85,10 @@ export function toDataShapeKinds( /** * returns an empty integration object. * - * @todo make the returned object immutable to avoid uncontrolled changes */ export function getEmptyIntegration(): Integration { return produce(NEW_INTEGRATION, draft => { + draft.id = key(); draft.flows = [ { id: key(), @@ -1324,3 +1329,7 @@ export function isMiddleStep( !isEndStep(integration, flowId, position) ); } + +export function isIntegrationApiProvider(integration: IntegrationOverview) { + return (integration.tags || []).includes(API_PROVIDER); +} diff --git a/app/ui-react/packages/api/src/index.ts b/app/ui-react/packages/api/src/index.ts index b7f126fdaeb..3ded049b906 100644 --- a/app/ui-react/packages/api/src/index.ts +++ b/app/ui-react/packages/api/src/index.ts @@ -7,6 +7,7 @@ export * from './helpers'; export * from './ServerEventsContext'; export * from './Stream'; export * from './SyndesisFetch'; +export * from './useApiProvider'; export * from './useIntegrationHelpers'; export * from './WithActionDescriptor'; export * from './WithActivities'; diff --git a/app/ui-react/packages/api/src/useApiProvider.tsx b/app/ui-react/packages/api/src/useApiProvider.tsx new file mode 100644 index 00000000000..fe389a38744 --- /dev/null +++ b/app/ui-react/packages/api/src/useApiProvider.tsx @@ -0,0 +1,67 @@ +import { APISummary } from '@syndesis/models'; +import * as React from 'react'; +import { ApiContext } from './ApiContext'; +import { callFetch } from './callFetch'; + +export function useApiProviderSummary(specification: string) { + const apiContext = React.useContext(ApiContext); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(false); + const [apiSummary, setApiSummary] = React.useState( + undefined + ); + + React.useEffect(() => { + const fetchSummary = async () => { + setLoading(true); + try { + const body = new FormData(); + body.append('specification', specification); + const response = await callFetch({ + body, + headers: apiContext.headers, + includeAccept: true, + includeContentType: false, + method: 'POST', + url: `${apiContext.apiUri}/apis/info`, + }); + const summary = await response.json(); + if (summary.errorCode) { + throw new Error(summary.userMsg); + } + setApiSummary(summary as APISummary); + } catch (e) { + setError(e as Error); + } finally { + setLoading(false); + } + }; + fetchSummary(); + }, [specification, apiContext, setLoading]); + + return { apiSummary, loading, error }; +} + +export function useApiProviderIntegration() { + const apiContext = React.useContext(ApiContext); + + const getIntegration = async (specification: string) => { + const body = new FormData(); + body.append('specification', specification); + const response = await callFetch({ + body, + headers: apiContext.headers, + includeAccept: true, + includeContentType: false, + method: 'POST', + url: `${apiContext.apiUri}/apis/generator`, + }); + const integration = await response.json(); + if (integration.errorCode) { + throw new Error(integration.userMsg); + } + return integration; + }; + + return getIntegration; +} diff --git a/app/ui-react/packages/api/src/useIntegrationHelpers.tsx b/app/ui-react/packages/api/src/useIntegrationHelpers.tsx index 13774fb3015..1b063633104 100644 --- a/app/ui-react/packages/api/src/useIntegrationHelpers.tsx +++ b/app/ui-react/packages/api/src/useIntegrationHelpers.tsx @@ -182,6 +182,10 @@ export const useIntegrationHelpers = () => { } }; + /** + * Uploads and imports the supplied OpenAPI specification + */ + /** * Requests a .zip file of the integration, using the specified filename * @param id diff --git a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLayout.tsx b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLayout.tsx index c0eb37cf440..878129f8924 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLayout.tsx +++ b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLayout.tsx @@ -40,6 +40,7 @@ import './IntegrationEditorLayout.css'; export interface IIntegrationEditorLayoutProps { title: string; description?: string; + toolbar?: React.ReactNode; sidebar?: React.ReactNode; content: React.ReactNode; onCancel?: (e: React.MouseEvent) => void; @@ -71,6 +72,7 @@ export const IntegrationEditorLayout: React.FunctionComponent< > = ({ title, description, + toolbar, sidebar, content, onPublish, @@ -89,6 +91,7 @@ export const IntegrationEditorLayout: React.FunctionComponent<
+ {toolbar} diff --git a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderEditSpecification.tsx b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderEditSpecification.tsx deleted file mode 100644 index 91770ddb1e6..00000000000 --- a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderEditSpecification.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react'; - -export interface IApiProviderEditSpecificationProps { - /** - * The title - */ - i18nTitle?: string; -} - -export class ApiProviderEditSpecification extends React.Component< - IApiProviderEditSpecificationProps -> { - public render() { - return ( - <> -
{this.props.i18nTitle}
- {this.props.children} - - ); - } -} diff --git a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewActions.tsx b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewActions.tsx deleted file mode 100644 index 6c8eb335dc8..00000000000 --- a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewActions.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { ListView } from 'patternfly-react'; -import * as React from 'react'; - -export interface IApiProviderReviewActionsProps { - /** - * The title - */ - i18nTitle?: string; -} - -export class ApiProviderReviewActions extends React.Component< - IApiProviderReviewActionsProps -> { - public render() { - return ( - <> -
{this.props.i18nTitle}
- - } - leftContent={} - additionalInfo={[ - Item 1, - , - ]} - stacked={false} - > - Expanded Content - - } - heading={ - - This is EVENT One that is with very LONG and should not overflow - and push other elements out of the bounding box. - Feb 23, 2015 12:32 am - - } - actions={
} - description={ - - The following snippet of text is rendered as{' '} - link text. - - } - stacked={false} - /> - } - heading="Stacked Additional Info items" - description={ - - The following snippet of text is rendered as{' '} - link text. - - } - additionalInfo={[ - - 113,735 - Service One - , - - 35% - Service Two - , - ]} - stacked={false} - /> - Only Additional, - Info Items, - ]} - stacked={true} - /> - - - ); - } -} diff --git a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewOperations.tsx b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewOperations.tsx index d808c3d60b1..692ecc06c19 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewOperations.tsx +++ b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewOperations.tsx @@ -1,16 +1,18 @@ +import { PageSection } from '@patternfly/react-core'; +import { ListView } from 'patternfly-react'; import * as React from 'react'; - -export interface IApiProviderReviewOperationsProps { - /** - * The title - */ - i18nTitle?: string; -} +import { IListViewToolbarProps, ListViewToolbar } from '../../../Shared'; export class ApiProviderReviewOperations extends React.Component< - IApiProviderReviewOperationsProps + IListViewToolbarProps > { public render() { - return <>{this.props.i18nTitle}; + const { children, ...props } = this.props; + return ( + + + {children} + + ); } } diff --git a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewOperationsItem.tsx b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewOperationsItem.tsx new file mode 100644 index 00000000000..5ab812964ef --- /dev/null +++ b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderReviewOperationsItem.tsx @@ -0,0 +1,43 @@ +import * as H from '@syndesis/history'; +import { ListView } from 'patternfly-react'; +import * as React from 'react'; +import { ButtonLink } from '../../../Layout'; +import { HttpMethodColors } from '../../../Shared'; + +export interface IApiProviderReviewOperationsItemProps { + createAsPrimary: boolean; + createFlowHref: H.LocationDescriptor; + i18nCreateFlow: string; + onCreateFlow?: (e: React.MouseEvent) => void; + operationDescription: string; + operationHttpMethod: string; + operationPath: string; +} + +export class ApiProviderReviewOperationsItem extends React.Component< + IApiProviderReviewOperationsItemProps +> { + public render() { + return ( + + {this.props.i18nCreateFlow} + + } + className={'api-provider-operations-list-item'} + heading={this.props.operationPath} + description={this.props.operationDescription} + leftContent={ + + } + stacked={false} + /> + ); + } +} diff --git a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderSelectMethod.tsx b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderSelectMethod.tsx deleted file mode 100644 index f944946240c..00000000000 --- a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderSelectMethod.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Text } from '@patternfly/react-core'; -import { ListView } from 'patternfly-react'; -import * as React from 'react'; - -export interface IApiProviderSelectMethodProps { - i18nDescription?: string; - /** - * The title - */ - i18nTitle?: string; -} - -export class ApiProviderSelectMethod extends React.Component< - IApiProviderSelectMethodProps -> { - public render() { - return ( - <> - {this.props.i18nTitle} - {this.props.i18nDescription} - - } - leftContent={} - additionalInfo={[ - Item 1, - , - ]} - stacked={false} - > - Expanded Content - - } - heading={ - - This is EVENT One that is with very LONG and should not overflow - and push other elements out of the bounding box. - Feb 23, 2015 12:32 am - - } - actions={
} - description={ - - The following snippet of text is rendered as{' '} - link text. - - } - stacked={false} - /> - } - heading="Stacked Additional Info items" - description={ - - The following snippet of text is rendered as{' '} - link text. - - } - additionalInfo={[ - - 113,735 - Service One - , - - 35% - Service Two - , - ]} - stacked={false} - /> - Only Additional, - Info Items, - ]} - stacked={true} - /> - - - ); - } -} diff --git a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderSetInfo.tsx b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderSetInfo.tsx index 1171f79e15a..5515118cbe4 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderSetInfo.tsx +++ b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderSetInfo.tsx @@ -1,16 +1,32 @@ import * as React from 'react'; +import { Container } from '../../../Layout'; export interface IApiProviderSetInfoProps { /** - * The title + * The callback fired when submitting the form. + * @param e */ - i18nTitle?: string; + handleSubmit: (e?: any) => void; } export class ApiProviderSetInfo extends React.Component< IApiProviderSetInfoProps > { public render() { - return <>{this.props.i18nTitle}; + return ( + +
+
+
+
{this.props.children}
+
+
+
+
+ ); } } diff --git a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderUploader.tsx b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderUploader.tsx deleted file mode 100644 index 688a8bdfe32..00000000000 --- a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/ApiProviderUploader.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Card } from 'patternfly-react'; -import * as React from 'react'; -import { DndFileChooser } from '../../../Shared'; - -export interface IApiProviderUploaderProps { - /** - * `true` if the dropzone should be disabled. - */ - dndDisabled: boolean; - - /** - * The localized text that appears below the selected file information at the bottom of the drop area. This - * message should identify the accepted file extension. - */ - i18nDndHelpMessage: string; - - /** - * The localized text (may include HTML tags) that appears above the selected file information at the - * top of the drop area. This message should describe how the DnD works. - */ - i18nDndInstructions: string; - - /** - * The localized text that appears when no file has been selected. - */ - i18nDndNoFileSelectedMessage: string; - - /** - * The localized text for the label that appears before the selected file information. - */ - i18nDndSelectedFileLabel: string; - - /** - * A general, localized message for when a file upload failed. This message will be shown - * along with an error icon and appears next to the selected file message. If this property - * is set then `i18nDndUploadSuccessMessage` should not be set. - */ - i18nDndUploadFailedMessage?: string; - - /** - * A general, localized message for when a file upload was successful. This message will be shown - * along with an OK icon and appear next to the selected file message. If this property - * is set then `i18nDndUploadFailedMessage` should not be set. - */ - i18nDndUploadSuccessMessage?: string; - - /** - * The title - */ - i18nTitle: string; - - /** - * Callback for when one or more file uploads have been accepted. Caller should handler processing of the files. - */ - onDndUploadAccepted(files: File[]): void; - - /** - * The localized text (may include HTML tags) that appears when the file upload fails. - */ - onDndUploadRejected(fileName: string): string; -} - -export class ApiProviderUploader extends React.Component< - IApiProviderUploaderProps -> { - public render() { - return ( - /** - * TODO: Move drag and drop stuff to its own component, - * keep this is as a list of options: Upload or Use a URL - */ - - - {this.props.i18nTitle} - - - - - - ); - } -} diff --git a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/index.ts b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/index.ts index b90dfeb1a65..0b21fcd55d0 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/index.ts +++ b/app/ui-react/packages/ui/src/Integration/Editor/apiProvider/index.ts @@ -1,6 +1,3 @@ -export * from './ApiProviderEditSpecification'; -export * from './ApiProviderReviewActions'; export * from './ApiProviderReviewOperations'; -export * from './ApiProviderSelectMethod'; +export * from './ApiProviderReviewOperationsItem'; export * from './ApiProviderSetInfo'; -export * from './ApiProviderUploader'; diff --git a/app/ui-react/packages/ui/src/Integration/OperationsDropdown.tsx b/app/ui-react/packages/ui/src/Integration/OperationsDropdown.tsx new file mode 100644 index 00000000000..601704eacba --- /dev/null +++ b/app/ui-react/packages/ui/src/Integration/OperationsDropdown.tsx @@ -0,0 +1,26 @@ +import { Dropdown, DropdownToggle } from '@patternfly/react-core'; +import * as React from 'react'; + +export interface IOperationsDropdownProps { + selectedOperation: React.ReactNode; +} +export const OperationsDropdown: React.FunctionComponent< + IOperationsDropdownProps +> = ({ selectedOperation, children }) => { + const [isOpen, setIsOpen] = React.useState(false); + const closeDropdown = () => setIsOpen(false); + const toggleDropdown = () => setIsOpen(!isOpen); + return ( + + {selectedOperation} + + } + isOpen={isOpen} + > +
{children}
+
+ ); +}; diff --git a/app/ui-react/packages/ui/src/Integration/index.ts b/app/ui-react/packages/ui/src/Integration/index.ts index 7855aeccf11..6fbe85488cf 100644 --- a/app/ui-react/packages/ui/src/Integration/index.ts +++ b/app/ui-react/packages/ui/src/Integration/index.ts @@ -22,3 +22,4 @@ export * from './IntegrationsListItemBasic'; export * from './IntegrationsListItemUnreadable'; export * from './IntegrationsListSkeleton'; export * from './IntegrationsListView'; +export * from './OperationsDropdown'; diff --git a/app/ui-react/packages/ui/src/Layout/Breadcrumb.tsx b/app/ui-react/packages/ui/src/Layout/Breadcrumb.tsx index 820dab901de..449d45cfa5a 100644 --- a/app/ui-react/packages/ui/src/Layout/Breadcrumb.tsx +++ b/app/ui-react/packages/ui/src/Layout/Breadcrumb.tsx @@ -24,13 +24,14 @@ export const Breadcrumb: React.FunctionComponent = ({ actions, children, }) => { - const count = React.Children.count(children); + const items = React.Children.toArray(children).filter(c => c); + const count = items.length; return ( - {React.Children.map(children, (c, idx) => ( + {items.map((c, idx) => ( {c} diff --git a/app/ui-react/packages/ui/src/Shared/DndFileChooser.tsx b/app/ui-react/packages/ui/src/Shared/DndFileChooser.tsx index 35d8806aeab..eb73b6f4351 100644 --- a/app/ui-react/packages/ui/src/Shared/DndFileChooser.tsx +++ b/app/ui-react/packages/ui/src/Shared/DndFileChooser.tsx @@ -78,7 +78,7 @@ export interface IDndFileChooserProps { onUploadRejected(fileName: string): string; /** - * Callback for when one or more file uploads have been accepted. Caller should handler processing of the files. + * Callback for when one or more file uploads have been accepted. Caller should handle processing of the files. */ onUploadAccepted(file: File[]): void; } diff --git a/app/ui-react/packages/ui/src/Shared/HttpMethodColors.css b/app/ui-react/packages/ui/src/Shared/HttpMethodColors.css new file mode 100644 index 00000000000..73f30711a8f --- /dev/null +++ b/app/ui-react/packages/ui/src/Shared/HttpMethodColors.css @@ -0,0 +1,20 @@ +.http-method--delete { + color: #D01414; + font-weight: 600; +} + +.http-method--get { + color: #3C96D4; + font-weight: 600; +} + +.http-method--post { + color: #61AF5A; + font-weight: 600; +} + +.http-method--put { + color: #ED811D; + font-weight: 600; +} + diff --git a/app/ui-react/packages/ui/src/Shared/HttpMethodColors.tsx b/app/ui-react/packages/ui/src/Shared/HttpMethodColors.tsx new file mode 100644 index 00000000000..2459f51fc22 --- /dev/null +++ b/app/ui-react/packages/ui/src/Shared/HttpMethodColors.tsx @@ -0,0 +1,26 @@ +import classNames from 'classnames'; +import * as React from 'react'; + +import './HttpMethodColors.css'; + +export interface IHttpMethodColorsProps { + method: string; +} + +/** + * Component that returns an HTTP method + * with the properly formatted color. + * Accepts a string for the HTTP method in question. + */ +export class HttpMethodColors extends React.Component { + public render() { + const httpMethodClass = classNames({ + 'http-method--delete': this.props.method === 'DELETE', + 'http-method--get': this.props.method === 'GET', + 'http-method--post': this.props.method === 'POST', + 'http-method--put': this.props.method === 'PUT', + }); + + return {this.props.method}; + } +} diff --git a/app/ui-react/packages/ui/src/Shared/IframeWrapper.tsx b/app/ui-react/packages/ui/src/Shared/IframeWrapper.tsx new file mode 100644 index 00000000000..036bc3c7fec --- /dev/null +++ b/app/ui-react/packages/ui/src/Shared/IframeWrapper.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { PageSection } from '../Layout'; + +export class IframeWrapper extends React.Component { + public render() { + return ( + +
+ {this.props.children} +
+
+ ); + } +} diff --git a/app/ui-react/packages/ui/src/Shared/ListViewToolbar.tsx b/app/ui-react/packages/ui/src/Shared/ListViewToolbar.tsx index d9c354ad40d..82024960968 100644 --- a/app/ui-react/packages/ui/src/Shared/ListViewToolbar.tsx +++ b/app/ui-react/packages/ui/src/Shared/ListViewToolbar.tsx @@ -23,6 +23,7 @@ export interface ISortType { } export interface IActiveFilter { + id: string; title: string; value: string; } @@ -31,7 +32,7 @@ export interface IListViewToolbarProps { activeFilters: IActiveFilter[]; filterTypes: IFilterType[]; currentFilterType: IFilterType; - currentSortType: string; + currentSortType: ISortType; currentValue: any; isSortAscending: boolean; resultsCount: number; @@ -42,7 +43,7 @@ export interface IListViewToolbarProps { onValueKeyPress(keyEvent: KeyboardEvent): void; - onFilterAdded(title: string, value: string): void; + onFilterAdded(id: string, title: string, value: string): void; onSelectFilterType(filterType: IFilterType): void; @@ -54,7 +55,7 @@ export interface IListViewToolbarProps { onToggleCurrentSortDirection(): void; - onUpdateCurrentSortType(sortType: string): void; + onUpdateCurrentSortType(sortType: ISortType): void; } export class ListViewToolbar extends React.Component { diff --git a/app/ui-react/packages/ui/src/Shared/OpenApiReviewActions.css b/app/ui-react/packages/ui/src/Shared/OpenApiReviewActions.css new file mode 100644 index 00000000000..0baf0fddb48 --- /dev/null +++ b/app/ui-react/packages/ui/src/Shared/OpenApiReviewActions.css @@ -0,0 +1,15 @@ +.open-api-review-actions .review-actions__heading { + background-color: #f5f5f5; + font-weight: bold; + margin-bottom: 8px; + margin-top: 8px; + padding: 4px 8px; +} + +.open-api-review-actions .review-actions__tagMessageList { + --pf-c-content--ul--ListStyle: none; +} + +.open-api-review-actions .review-actions__validationFallbackMessage { + font-weight: bold; +} diff --git a/app/ui-react/packages/ui/src/Shared/OpenApiReviewActions.tsx b/app/ui-react/packages/ui/src/Shared/OpenApiReviewActions.tsx new file mode 100644 index 00000000000..d848ff72bd2 --- /dev/null +++ b/app/ui-react/packages/ui/src/Shared/OpenApiReviewActions.tsx @@ -0,0 +1,144 @@ +import { + Text, + TextContent, + TextList, + TextListItem, + TextListItemVariants, + TextListVariants, + TextVariants, + Title, +} from '@patternfly/react-core'; +import { Card } from 'patternfly-react'; +import * as React from 'react'; +import { Container } from '../Layout'; + +import './OpenApiReviewActions.css'; + +export interface IApiProviderReviewActionsProps { + apiProviderDescription?: string; + apiProviderName?: string; + errorMessages?: string[]; + i18nApiDefinitionHeading: string; + i18nDescriptionLabel: string; + i18nErrorsHeading?: string; + i18nImportedHeading: string; + i18nNameLabel: string; + i18nOperationsHtmlMessage: string; + i18nOperationTagHtmlMessages?: string[]; + i18nValidationFallbackMessage?: string; + i18nWarningsHeading?: string; + warningMessages?: string[]; +} + +export class OpenApiReviewActions extends React.Component< + IApiProviderReviewActionsProps +> { + public render() { + return ( + + + {this.props.i18nValidationFallbackMessage ? ( +
+ {this.props.i18nValidationFallbackMessage} +
+ ) : ( + + + {this.props.i18nApiDefinitionHeading} + + + + + {this.props.i18nNameLabel} + + + {this.props.apiProviderName} + + + {this.props.i18nDescriptionLabel} + + + {this.props.apiProviderDescription} + + + + + {this.props.i18nImportedHeading} + + + + + + {/* tagged messages */} + {this.props.i18nOperationTagHtmlMessages && ( + + {this.props.i18nOperationTagHtmlMessages.map( + (msg: string, index: number) => ( + + ) + )} + + )} + + {/* error messages */} + {this.props.i18nErrorsHeading && this.props.errorMessages && ( + + {this.props.i18nErrorsHeading} + + )} + {this.props.errorMessages + ? this.props.errorMessages.map( + (errorMsg: string, index: number) => ( + + {index + 1}. {errorMsg} + + ) + ) + : null} + + {/* warning messages */} + {this.props.i18nWarningsHeading && this.props.warningMessages && ( + + {this.props.i18nWarningsHeading} + + )} + {this.props.warningMessages + ? this.props.warningMessages.map( + (warningMsg: string, index: number) => ( + + {index + 1}. {warningMsg} + + ) + ) + : null} + + )} +
+
+ ); + } +} diff --git a/app/ui-react/packages/ui/src/Shared/OpenApiSelectMethod.css b/app/ui-react/packages/ui/src/Shared/OpenApiSelectMethod.css new file mode 100644 index 00000000000..71c7058ff83 --- /dev/null +++ b/app/ui-react/packages/ui/src/Shared/OpenApiSelectMethod.css @@ -0,0 +1,5 @@ +.open-api-select-method .url-note { + color: gray; + font-size: smaller; + font-style: italic; +} diff --git a/app/ui-react/packages/ui/src/Shared/OpenApiSelectMethod.tsx b/app/ui-react/packages/ui/src/Shared/OpenApiSelectMethod.tsx new file mode 100644 index 00000000000..2341921993b --- /dev/null +++ b/app/ui-react/packages/ui/src/Shared/OpenApiSelectMethod.tsx @@ -0,0 +1,220 @@ +// tslint:disable:no-console +import { + Col, + FormControl, + FormGroup, + Grid, + Radio, + Row, +} from 'patternfly-react'; +import * as React from 'react'; +import { ButtonLink, Container } from '../Layout'; +import { DndFileChooser } from './DndFileChooser'; +import './OpenApiSelectMethod.css'; + +export type Method = 'file' | 'url' | 'scratch'; + +export interface IOpenApiSelectMethodProps { + allowMultiple?: boolean; + disableDropzone: boolean; + fileExtensions?: string; + /** + * Localized strings to be displayed. + */ + i18nBtnNext: string; + i18nHelpMessage?: string; + i18nInstructions: string; + i18nMethodFromFile: string; + i18nMethodFromUrl: string; + i18nMethodFromScratch: string; + i18nNoFileSelectedMessage: string; + i18nSelectedFileLabel: string; + i18nUploadFailedMessage?: string; + i18nUploadFailedMessages?: string[]; + i18nUploadSuccessMessage?: string; + i18nUploadSuccessMessages?: string[]; + i18nUrlNote: string; + + /** + * The action fired when the user presses the Next button. + */ + onNext(method?: Method, specification?: string): void; +} + +export interface IOpenApiSelectMethodState { + method?: Method; + specification?: string; + valid?: boolean; +} + +export class OpenApiSelectMethod extends React.Component< + IOpenApiSelectMethodProps, + IOpenApiSelectMethodState +> { + constructor(props: any) { + super(props); + this.state = { + method: 'file', + specification: '', + valid: false, + }; + + this.onAddUrlSpecification = this.onAddUrlSpecification.bind(this); + this.onSelectMethod = this.onSelectMethod.bind(this); + this.onUploadAccepted = this.onUploadAccepted.bind(this); + this.onUploadRejected = this.onUploadRejected.bind(this); + } + + public checkValidUrl(url: string): boolean { + const regexp = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; + return regexp.test(url); + } + + /** + * User has added a specification via a string URL, which will be + * checked if is a valid HTTP/HTTPS string. + * @param e + */ + public onAddUrlSpecification(e: React.FormEvent) { + this.setState({ specification: e.currentTarget.value }); + + if ( + this.state.method === 'url' && + this.checkValidUrl(e.currentTarget.value) + ) { + this.setState({ valid: true }); + } else { + this.setState({ valid: false }); + } + } + + /** + * The action fired when the user selects the method + * to provide an OpenAPI specification. + * @param newMethod + */ + public onSelectMethod(newMethod: Method) { + this.setState({ + method: newMethod, + specification: '', + valid: newMethod === 'scratch', + }); + } + + /** + * Callback for when one or more file uploads have been accepted. + */ + public onUploadAccepted(files: File[]): void { + const reader = new FileReader(); + reader.readAsText(files[0]); + reader.onload = () => { + this.setState({ specification: reader.result as string, valid: true }); + }; + } + + /** + * Obtains the localized text (may include HTML tags) that appears when the file upload was rejected. This + * will occur when a DnD of a file with the wrong extension is dropped. This message is presented + * as a timed toast notification. + */ + public onUploadRejected(fileName: string): string { + return ( + 'File ' + + fileName + + ' could not be uploaded' + ); + } + + public render() { + return ( + + + + +
+ this.onSelectMethod('file')} + checked={this.state.method === 'file'} + readOnly={true} + > +
{this.props.i18nMethodFromFile}
+ {this.state.method === 'file' && ( + + + + )} +
+ this.onSelectMethod('url')} + readOnly={true} + > +
{this.props.i18nMethodFromUrl}
+ {this.state.method === 'url' && ( +
+ +
+ + {this.props.i18nUrlNote} + +
+ )} +
+ this.onSelectMethod('scratch')} + readOnly={true} + > +
{this.props.i18nMethodFromScratch}
+
+
+
+ + + + this.props.onNext(this.state.method, this.state.specification) + } + > + {this.props.i18nBtnNext} + +
+
+ ); + } +} diff --git a/app/ui-react/packages/ui/src/Shared/index.ts b/app/ui-react/packages/ui/src/Shared/index.ts index 588cc2bba1f..95fca47b0cc 100644 --- a/app/ui-react/packages/ui/src/Shared/index.ts +++ b/app/ui-react/packages/ui/src/Shared/index.ts @@ -7,14 +7,18 @@ export * from './Dialog'; export * from './DndFileChooser'; export * from './GenericTable'; export * from './HelpDropdown'; +export * from './HttpMethodColors'; export * from './InlineTextEdit'; export * from './ListViewToolbar'; export * from './LogViewer'; export * from './models'; export * from './Notifications'; -export * from './SimplePageHeader'; -export * from './ProgressWithLink'; +export * from './OpenApiReviewActions'; +export * from './OpenApiSelectMethod'; export * from './PfDropdownItem'; +export * from './ProgressWithLink'; +export * from './SimplePageHeader'; export * from './TextEditor'; export * from './UnrecoverableError'; export * from './WithDropzone'; +export * from './IframeWrapper'; diff --git a/app/ui-react/packages/ui/src/index.css b/app/ui-react/packages/ui/src/index.css index 81f8c148e39..403ddb9f752 100644 --- a/app/ui-react/packages/ui/src/index.css +++ b/app/ui-react/packages/ui/src/index.css @@ -36,8 +36,7 @@ body { } #root { - flex: 1; - display: flex; + height: 100%; } .navbar-pf .navbar-utility > li > button { diff --git a/app/ui-react/packages/ui/stories/Connection/ConnectionsListView.stories.tsx b/app/ui-react/packages/ui/stories/Connection/ConnectionsListView.stories.tsx index d877ce4e5b5..add4c0bf67c 100644 --- a/app/ui-react/packages/ui/stories/Connection/ConnectionsListView.stories.tsx +++ b/app/ui-react/packages/ui/stories/Connection/ConnectionsListView.stories.tsx @@ -88,7 +88,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -123,7 +127,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} diff --git a/app/ui-react/packages/ui/stories/Customization/ApiConnectorListView.stories.tsx b/app/ui-react/packages/ui/stories/Customization/ApiConnectorListView.stories.tsx index 05a089afce6..5bab2676470 100644 --- a/app/ui-react/packages/ui/stories/Customization/ApiConnectorListView.stories.tsx +++ b/app/ui-react/packages/ui/stories/Customization/ApiConnectorListView.stories.tsx @@ -122,7 +122,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -169,7 +173,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} diff --git a/app/ui-react/packages/ui/stories/Customization/ExtensionListView.stories.tsx b/app/ui-react/packages/ui/stories/Customization/ExtensionListView.stories.tsx index b705a106fe3..6c3349bf752 100644 --- a/app/ui-react/packages/ui/stories/Customization/ExtensionListView.stories.tsx +++ b/app/ui-react/packages/ui/stories/Customization/ExtensionListView.stories.tsx @@ -143,7 +143,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -190,7 +194,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} diff --git a/app/ui-react/packages/ui/stories/Customization/ExtensionsPage.stories.tsx b/app/ui-react/packages/ui/stories/Customization/ExtensionsPage.stories.tsx index 6b9c4fd44d2..b89d9cdee8b 100644 --- a/app/ui-react/packages/ui/stories/Customization/ExtensionsPage.stories.tsx +++ b/app/ui-react/packages/ui/stories/Customization/ExtensionsPage.stories.tsx @@ -38,7 +38,11 @@ storiesOf('Customization/Extensions/ExtensionsPage', module) placeholder: '', title: '', }} - currentSortType={'whatever'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} isSortAscending={true} resultsCount={1} @@ -113,7 +117,11 @@ storiesOf('Customization/Extensions/ExtensionsPage', module) placeholder: '', title: '', }} - currentSortType={'whatever'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} isSortAscending={true} resultsCount={1} diff --git a/app/ui-react/packages/ui/stories/Data/Views/ViewInfoList.stories.tsx b/app/ui-react/packages/ui/stories/Data/Views/ViewInfoList.stories.tsx index c5dc4317455..961c90e51f1 100644 --- a/app/ui-react/packages/ui/stories/Data/Views/ViewInfoList.stories.tsx +++ b/app/ui-react/packages/ui/stories/Data/Views/ViewInfoList.stories.tsx @@ -70,7 +70,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -114,7 +118,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} diff --git a/app/ui-react/packages/ui/stories/Data/Views/ViewList.stories.tsx b/app/ui-react/packages/ui/stories/Data/Views/ViewList.stories.tsx index ef055fb3e13..5607dac7340 100644 --- a/app/ui-react/packages/ui/stories/Data/Views/ViewList.stories.tsx +++ b/app/ui-react/packages/ui/stories/Data/Views/ViewList.stories.tsx @@ -135,7 +135,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -187,7 +191,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} diff --git a/app/ui-react/packages/ui/stories/Data/Virtualizations/VirtualizationList.stories.tsx b/app/ui-react/packages/ui/stories/Data/Virtualizations/VirtualizationList.stories.tsx index 0520a6a96d6..34634141f33 100644 --- a/app/ui-react/packages/ui/stories/Data/Virtualizations/VirtualizationList.stories.tsx +++ b/app/ui-react/packages/ui/stories/Data/Virtualizations/VirtualizationList.stories.tsx @@ -193,7 +193,7 @@ const virtualizationItems = [ i18nEdit={editText} i18nEditTip={editTip2} i18nError={errorText} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP i18nExport={exportText} */ i18nPublished={publishedText} i18nUnpublish={unpublishText} @@ -203,7 +203,7 @@ const virtualizationItems = [ i18nUnpublishModalMessage={confirmUnpublishMessage} i18nUnpublishModalTitle={confirmUnpublishTitle} onDelete={action(deleteText)} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP onExport={action(exportText)} */ onUnpublish={action(unpublishText)} onPublish={action(publishText)} @@ -297,7 +297,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -324,8 +328,8 @@ stories 'i18nEmptyStateTitle', createVirtualization )} - /* TD-636: Commented out for TP - i18nImport={importText} + /* TD-636: Commented out for TP + i18nImport={importText} i18nImportTip={importTip} */ i18nLinkCreateVirtualization={text( 'i18nLinkCreateVirtualization', @@ -339,7 +343,7 @@ stories )} i18nResultsCount={text('i18nResultsCount', '0 Results')} i18nTitle={text('i18nTitle', title)} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP onImport={action(importText)} */ children={[]} hasListData={false} @@ -360,7 +364,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -387,7 +395,7 @@ stories 'i18nEmptyStateTitle', createVirtualization )} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP i18nImport={importText} i18nImportTip={importTip} */ i18nLinkCreateVirtualization={text( @@ -404,7 +412,7 @@ stories virtualizationItems.length + ' Results' )} i18nTitle={text('i18nTitle', title)} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP onImport={action(importText)} */ children={virtualizationItems} hasListData={true} @@ -425,7 +433,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -452,7 +464,7 @@ stories 'i18nEmptyStateTitle', createVirtualization )} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP i18nImport={importText} i18nImportTip={importTip} */ i18nLinkCreateVirtualization={text( @@ -469,7 +481,7 @@ stories virtItem.length + ' Results' )} i18nTitle={text('i18nTitle', title)} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP onImport={action(importText)} */ hasListData={true} /> diff --git a/app/ui-react/packages/ui/stories/Integration/CiCd/CiCdListView.stories.tsx b/app/ui-react/packages/ui/stories/Integration/CiCd/CiCdListView.stories.tsx index 01b0373c6ba..3559327f96a 100644 --- a/app/ui-react/packages/ui/stories/Integration/CiCd/CiCdListView.stories.tsx +++ b/app/ui-react/packages/ui/stories/Integration/CiCd/CiCdListView.stories.tsx @@ -18,7 +18,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} @@ -71,7 +75,11 @@ stories placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} diff --git a/app/ui-react/packages/ui/stories/Integration/CiCd/CiCdManagePageUI.stories.tsx b/app/ui-react/packages/ui/stories/Integration/CiCd/CiCdManagePageUI.stories.tsx index 5111e4ec048..e5db2c71244 100644 --- a/app/ui-react/packages/ui/stories/Integration/CiCd/CiCdManagePageUI.stories.tsx +++ b/app/ui-react/packages/ui/stories/Integration/CiCd/CiCdManagePageUI.stories.tsx @@ -112,7 +112,11 @@ class CiCdManagePageStory extends React.Component< placeholder: text('placeholder', 'Filter by name'), title: text('title', 'Name'), }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} filterTypes={[]} isSortAscending={true} diff --git a/app/ui-react/packages/ui/stories/Integration/Editor/ApiProvider.stories.tsx b/app/ui-react/packages/ui/stories/Integration/Editor/ApiProvider.stories.tsx new file mode 100644 index 00000000000..d59ac952e08 --- /dev/null +++ b/app/ui-react/packages/ui/stories/Integration/Editor/ApiProvider.stories.tsx @@ -0,0 +1,55 @@ +import { action } from '@storybook/addon-actions'; +import { boolean, text } from '@storybook/addon-knobs'; +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; + +import { OpenApiReviewActions, OpenApiSelectMethod } from '../../../src'; + +const fileExtensions = '.json, .yaml and .yml'; +const helpMessage = 'Accepted file type: .json, .yaml and .yml'; +const instructions = + "Drag 'n' drop a file here, or click to select a file using a file chooser dialog."; +const noFileSelectedMessage = 'no file selected'; + +const selectedFileLabel = 'Selected file:'; + +const stories = storiesOf('Integration/Editor/ApiProvider', module); + +stories + .add('Select Method', () => ( + + )) + .add('Review Actions', () => ( + 49 operations'} + i18nOperationTagHtmlMessages={[ + '- 1 tagged KIE Server Script :: Core', + '- 23 tagged User tasks administration :: BPM', + '- 24 tagged Queries - processes, nodes, variables and tasks :: BPM', + '- 1 tagged Rules evaluation :: Core', + ]} + /> + )); diff --git a/app/ui-react/packages/ui/stories/Integration/editor/steps/TemplateStep.stories.tsx b/app/ui-react/packages/ui/stories/Integration/Editor/steps/TemplateStep.stories.tsx similarity index 97% rename from app/ui-react/packages/ui/stories/Integration/editor/steps/TemplateStep.stories.tsx rename to app/ui-react/packages/ui/stories/Integration/Editor/steps/TemplateStep.stories.tsx index d20f8f7e320..af0e7907591 100644 --- a/app/ui-react/packages/ui/stories/Integration/editor/steps/TemplateStep.stories.tsx +++ b/app/ui-react/packages/ui/stories/Integration/Editor/steps/TemplateStep.stories.tsx @@ -9,7 +9,7 @@ import { } from '../../../../src'; import { ITextEditor } from '../../../../src/Shared'; -const stories = storiesOf('Integration/editor/step/TemplateStep', module); +const stories = storiesOf('Integration/Editor/step/TemplateStep', module); stories.add('Normal', () => ( <> diff --git a/app/ui-react/packages/ui/tests/ConnectionsListView.test.tsx b/app/ui-react/packages/ui/tests/ConnectionsListView.test.tsx index 95e691d084c..a55a14ffff4 100644 --- a/app/ui-react/packages/ui/tests/ConnectionsListView.test.tsx +++ b/app/ui-react/packages/ui/tests/ConnectionsListView.test.tsx @@ -17,7 +17,11 @@ it('renders the heading', () => { placeholder: 'foo placeholder', title: 'Foo', }} - currentSortType={'sort'} + currentSortType={{ + id: 'sort', + isNumeric: false, + title: 'Sort', + }} currentValue={''} isSortAscending={true} resultsCount={0} diff --git a/app/ui-react/packages/ui/tests/Customization/ExtensionListView.spec.tsx b/app/ui-react/packages/ui/tests/Customization/ExtensionListView.spec.tsx index 2f962566b59..af597e7c7e9 100644 --- a/app/ui-react/packages/ui/tests/Customization/ExtensionListView.spec.tsx +++ b/app/ui-react/packages/ui/tests/Customization/ExtensionListView.spec.tsx @@ -28,7 +28,11 @@ export default describe('ExtensionListView', () => { placeholder: filterPlaceholder, title: nameLabel, }, - currentSortType: 'sort', + currentSortType: { + id: 'sort', + isNumeric: false, + title: 'Sort', + }, currentValue: '', filterTypes: [], isSortAscending: true, diff --git a/app/ui-react/packages/utils/package.json b/app/ui-react/packages/utils/package.json index aee2fe92c66..483fb665703 100644 --- a/app/ui-react/packages/utils/package.json +++ b/app/ui-react/packages/utils/package.json @@ -60,6 +60,7 @@ "mustache": "3.0.1", "named-urls": "^1.4.0", "react-fast-compare": "^2.0.2", + "use-react-router": "^1.0.7", "velocityjs": "1.1.3" }, "jest": { diff --git a/app/ui-react/packages/utils/src/WithListViewToolbarHelpers.tsx b/app/ui-react/packages/utils/src/WithListViewToolbarHelpers.tsx index 5d6a8526129..5e174b93143 100644 --- a/app/ui-react/packages/utils/src/WithListViewToolbarHelpers.tsx +++ b/app/ui-react/packages/utils/src/WithListViewToolbarHelpers.tsx @@ -4,12 +4,12 @@ import * as React from 'react'; export interface IWithListViewToolbarHelpers extends IWithListViewToolbarHelpersState { onClearFilters(event: React.MouseEvent): void; - onFilterAdded(title: string, value: string): void; + onFilterAdded(id: string, title: string, value: string): void; onFilterValueSelected(filterValue: { id: string; title: string }): void; onRemoveFilter(filter: IActiveFilter): void; onSelectFilterType(filterType: IFilterType): void; onToggleCurrentSortDirection(): void; - onUpdateCurrentSortType(sortType: string): void; + onUpdateCurrentSortType(sortType: ISortType): void; onUpdateCurrentValue(event: Event): void; onValueKeyPress(keyEvent: KeyboardEvent): void; } @@ -17,13 +17,14 @@ export interface IWithListViewToolbarHelpers export interface IWithListViewToolbarHelpersProps { defaultFilterType: IFilterType; defaultSortType: ISortType; + defaultSortAscending?: boolean; children(props: IWithListViewToolbarHelpers): any; } export interface IWithListViewToolbarHelpersState { activeFilters: IActiveFilter[]; currentFilterType: IFilterType; - currentSortType: string; + currentSortType: ISortType; currentValue: any; filterCategory: any; isSortAscending: boolean; @@ -33,15 +34,19 @@ export class WithListViewToolbarHelpers extends React.Component< IWithListViewToolbarHelpersProps, IWithListViewToolbarHelpersState > { + public static defaultProps = { + defaultSortAscending: true, + }; + constructor(props: IWithListViewToolbarHelpersProps) { super(props); this.state = { activeFilters: [] as IActiveFilter[], currentFilterType: this.props.defaultFilterType, - currentSortType: this.props.defaultSortType.title, + currentSortType: this.props.defaultSortType, currentValue: '', filterCategory: null, - isSortAscending: true, + isSortAscending: this.props.defaultSortAscending!, }; } @@ -54,18 +59,23 @@ export class WithListViewToolbarHelpers extends React.Component< if (keyEvent.key === 'Enter' && currentValue && currentValue.length > 0) { this.setState({ currentValue: '' }); - this.onFilterAdded(currentFilterType.title, currentValue); + this.onFilterAdded( + currentFilterType.id, + currentFilterType.title, + currentValue + ); keyEvent.stopPropagation(); keyEvent.preventDefault(); } }; - public onFilterAdded = (title: string, value: string) => { + public onFilterAdded = (id: string, title: string, value: string) => { const { activeFilters } = this.state; this.setState({ activeFilters: [ ...activeFilters, { + id, title, value, } as IActiveFilter, @@ -88,7 +98,11 @@ export class WithListViewToolbarHelpers extends React.Component< this.setState({ currentValue: filterValue.title }); if (filterValue) { - this.onFilterAdded(currentFilterType.title, filterValue.title); + this.onFilterAdded( + currentFilterType.id, + currentFilterType.title, + filterValue.title + ); } }; @@ -116,10 +130,10 @@ export class WithListViewToolbarHelpers extends React.Component< this.setState({ isSortAscending: !isSortAscending }); }; - public onUpdateCurrentSortType = (sortType: string) => { + public onUpdateCurrentSortType = (sortType: ISortType) => { const { currentSortType } = this.state; - if (currentSortType !== sortType) { + if (currentSortType.id !== sortType.id) { this.setState({ currentSortType: sortType, isSortAscending: true, diff --git a/app/ui-react/packages/utils/src/index.ts b/app/ui-react/packages/utils/src/index.ts index ba0494a2f30..73a82fa7452 100644 --- a/app/ui-react/packages/utils/src/index.ts +++ b/app/ui-react/packages/utils/src/index.ts @@ -6,6 +6,7 @@ export * from './lintHelpers'; export * from './makeResolver'; export * from './WithListViewToolbarHelpers'; export * from './OptionalIntUtil'; +export * from './useRouteData'; export * from './WithLoader'; export * from './WithRouteData'; export * from './WithRouter'; diff --git a/app/ui-react/packages/utils/src/useRouteData.tsx b/app/ui-react/packages/utils/src/useRouteData.tsx new file mode 100644 index 00000000000..82812818a98 --- /dev/null +++ b/app/ui-react/packages/utils/src/useRouteData.tsx @@ -0,0 +1,13 @@ +import useReactRouter from 'use-react-router'; + +export function useRouteData() { + const { location, match, history } = useReactRouter(); + + return { + history, + location, + match, + params: match.params as P, + state: location.state as S, + }; +} diff --git a/app/ui-react/syndesis/src/i18n/locales/shared-translations.en.json b/app/ui-react/syndesis/src/i18n/locales/shared-translations.en.json index 3dc428d44e6..79f696b011e 100644 --- a/app/ui-react/syndesis/src/i18n/locales/shared-translations.en.json +++ b/app/ui-react/syndesis/src/i18n/locales/shared-translations.en.json @@ -42,6 +42,7 @@ "linkCreateIntegration": "Create Integration", "Name": "Name", "nameFilterPlaceholder": "Filter by Name...", + "Next": "Next", "Pending": "Pending", "Publish": "Publish", "Published": "Running", diff --git a/app/ui-react/syndesis/src/modules/apiClientConnectors/pages/ApiConnectorsPage.tsx b/app/ui-react/syndesis/src/modules/apiClientConnectors/pages/ApiConnectorsPage.tsx index 112e60cec56..7ec9827bad8 100644 --- a/app/ui-react/syndesis/src/modules/apiClientConnectors/pages/ApiConnectorsPage.tsx +++ b/app/ui-react/syndesis/src/modules/apiClientConnectors/pages/ApiConnectorsPage.tsx @@ -21,7 +21,7 @@ import routes from '../routes'; function getFilteredAndSortedApiConnectors( apiConnectors: Connector[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { let filteredAndSorted = apiConnectors; diff --git a/app/ui-react/syndesis/src/modules/connections/components/ConnectionsWithToolbar.tsx b/app/ui-react/syndesis/src/modules/connections/components/ConnectionsWithToolbar.tsx index 11ef639f94c..10560dcf79a 100644 --- a/app/ui-react/syndesis/src/modules/connections/components/ConnectionsWithToolbar.tsx +++ b/app/ui-react/syndesis/src/modules/connections/components/ConnectionsWithToolbar.tsx @@ -16,7 +16,7 @@ import { Connections, IConnectionsProps } from './Connections'; function getFilteredAndSortedConnections( connections: Connection[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { let filteredAndSortedConnections = connections; diff --git a/app/ui-react/syndesis/src/modules/data/pages/VirtualizationViewsPage.tsx b/app/ui-react/syndesis/src/modules/data/pages/VirtualizationViewsPage.tsx index fbb1a7128e8..0f296f8d00f 100644 --- a/app/ui-react/syndesis/src/modules/data/pages/VirtualizationViewsPage.tsx +++ b/app/ui-react/syndesis/src/modules/data/pages/VirtualizationViewsPage.tsx @@ -45,7 +45,7 @@ export interface IVirtualizationViewsPageRouteState { function getFilteredAndSortedViewDefns( viewDefinitions: ViewDefinition[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { let filteredAndSorted = viewDefinitions; diff --git a/app/ui-react/syndesis/src/modules/data/pages/VirtualizationsPage.tsx b/app/ui-react/syndesis/src/modules/data/pages/VirtualizationsPage.tsx index 599fcf2ad56..1321cae7430 100644 --- a/app/ui-react/syndesis/src/modules/data/pages/VirtualizationsPage.tsx +++ b/app/ui-react/syndesis/src/modules/data/pages/VirtualizationsPage.tsx @@ -20,7 +20,7 @@ import { getPublishingDetails } from '../shared/VirtualizationUtils'; function getFilteredAndSortedVirtualizations( virtualizations: RestDataService[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { let filteredAndSorted = virtualizations; @@ -81,7 +81,7 @@ export class VirtualizationsPage extends React.Component { alert('Import virtualization ' + virtualizationName); } - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP public handleExportVirtualization() { // TODO: implement handleExportVirtualization alert('Export virtualization '); @@ -216,7 +216,7 @@ export class VirtualizationsPage extends React.Component { i18nEmptyStateTitle={t( 'virtualization.emptyStateTitle' )} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP i18nImport={t('shared:Import')} i18nImportTip={t( 'virtualization.importVirtualizationTip' @@ -235,7 +235,7 @@ export class VirtualizationsPage extends React.Component { 'virtualization.virtualizationsPageTitle' )} linkCreateHRef={resolvers.virtualizations.create()} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP onImport={this.handleImportVirt} */ hasListData={data.length > 0} > @@ -299,7 +299,7 @@ export class VirtualizationsPage extends React.Component { 'virtualization.editDataVirtualizationTip' )} i18nError={t('shared:Error')} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP i18nExport={t('shared:Export')} */ i18nPublish={t( 'shared:Publish' @@ -321,7 +321,7 @@ export class VirtualizationsPage extends React.Component { 'virtualization.unpublishModalTitle' )} onDelete={handleDelete} - /* TD-636: Commented out for TP + /* TD-636: Commented out for TP onExport={ this .handleExportVirtualization diff --git a/app/ui-react/syndesis/src/modules/data/shared/DvConnectionsWithToolbar.tsx b/app/ui-react/syndesis/src/modules/data/shared/DvConnectionsWithToolbar.tsx index bb80e8eabc6..64fd5a0d511 100644 --- a/app/ui-react/syndesis/src/modules/data/shared/DvConnectionsWithToolbar.tsx +++ b/app/ui-react/syndesis/src/modules/data/shared/DvConnectionsWithToolbar.tsx @@ -19,7 +19,7 @@ function getFilteredAndSortedConnections( dvSourceStatuses: VirtualizationSourceStatus[], selectedConn: string, activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { // Connections are adjusted to supply dvStatus and selection diff --git a/app/ui-react/syndesis/src/modules/data/shared/ViewInfosContent.tsx b/app/ui-react/syndesis/src/modules/data/shared/ViewInfosContent.tsx index a80194d7adf..47d6afd7a7b 100644 --- a/app/ui-react/syndesis/src/modules/data/shared/ViewInfosContent.tsx +++ b/app/ui-react/syndesis/src/modules/data/shared/ViewInfosContent.tsx @@ -18,7 +18,7 @@ import { generateViewInfos } from './VirtualizationUtils'; function getFilteredAndSortedViewInfos( schemaNodes: SchemaNode[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean, selectedViewNames: string[], existingViewNames: string[] diff --git a/app/ui-react/syndesis/src/modules/extensions/pages/ExtensionsPage.tsx b/app/ui-react/syndesis/src/modules/extensions/pages/ExtensionsPage.tsx index 35f1f32d943..c7696300872 100644 --- a/app/ui-react/syndesis/src/modules/extensions/pages/ExtensionsPage.tsx +++ b/app/ui-react/syndesis/src/modules/extensions/pages/ExtensionsPage.tsx @@ -20,7 +20,7 @@ import { getExtensionTypeName } from '../utils'; function getFilteredAndSortedExtensions( extensions: Extension[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { let filteredAndSorted = extensions; diff --git a/app/ui-react/syndesis/src/modules/integrations/IntegrationCreatorApp.tsx b/app/ui-react/syndesis/src/modules/integrations/IntegrationCreatorApp.tsx index 0c5274eb924..478e3bfca8b 100644 --- a/app/ui-react/syndesis/src/modules/integrations/IntegrationCreatorApp.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/IntegrationCreatorApp.tsx @@ -1,15 +1,14 @@ import { ALL_STEPS, createStep, DATA_MAPPER } from '@syndesis/api'; import * as H from '@syndesis/history'; import { StepKind } from '@syndesis/models'; -import { Breadcrumb, toTestId } from '@syndesis/ui'; import * as React from 'react'; import { Translation } from 'react-i18next'; import { Route, Switch } from 'react-router'; -import { Link } from 'react-router-dom'; import { WithClosedNavigation } from '../../shared'; import { WithLeaveConfirmation } from '../../shared/WithLeaveConfirmation'; import { AddStepPage } from './components/editor/AddStepPage'; import { EditorApp } from './components/editor/EditorApp'; +import { OperationsPage } from './components/editor/OperationsPage'; import { SaveIntegrationPage } from './components/editor/SaveIntegrationPage'; import resolvers from './resolvers'; import routes from './routes'; @@ -31,8 +30,11 @@ const addStepPage = ( ...s, }) } - apiProviderHref={(step, p, s) => - resolvers.create.configure.editStep.apiProvider.selectMethod() + apiProviderHref={(step, params, state) => + resolvers.create.start.apiProvider.selectMethod({ + ...params, + ...state, + }) } connectionHref={(step, params, state) => resolvers.create.configure.editStep.connection.configureAction({ @@ -93,6 +95,12 @@ const addStepPage = ( ...s, }) } + rootHref={(p, s) => + resolvers.create.configure.entryPoint({ + ...p, + ...s, + }) + } /> ); @@ -109,6 +117,14 @@ const saveIntegrationPage = ( /> ); +const apiProviderOperationsPage = ( + resolvers.list()} + rootHref={(p, s) => resolvers.create.configure.entryPoint({ ...p, ...s })} + getFlowHref={(p, s) => resolvers.create.configure.index({ ...p, ...s })} + /> +); + /** * Entry point for the integration creator app. This is shown when an user clicks * the "Create integration" button somewhere in the app. @@ -132,18 +148,6 @@ const saveIntegrationPage = ( export const IntegrationCreatorApp: React.FunctionComponent = () => { return ( - - - Integrations - - New integration - {t => ( { appStepRoutes={routes.create.start} appResolvers={resolvers.create.start} cancelHref={resolvers.list} - postConfigureHref={(integration, params) => { - return resolvers.create.finish.selectStep({ - integration, - ...params, - position: '1', - }); - }} + postConfigureHref={( + integration, + params, + state, + isApiProvider + ) => + isApiProvider + ? resolvers.create.configure.operations({ + integration, + }) + : resolvers.create.finish.selectStep({ + integration, + ...params, + position: '1', + }) + } /> @@ -196,6 +209,12 @@ export const IntegrationCreatorApp: React.FunctionComponent = () => { children={addStepPage} /> + + {/* add step */} + resolvers.integration.edit.editStep.apiProvider.selectMethod({ + ...params, + ...state, + }) } getDeleteEdgeStepHref={(position, p, s) => resolvers.integration.edit.editStep.selectStep({ @@ -98,6 +100,12 @@ const addStepPage = ( ...s, }) } + rootHref={(p, s) => + resolvers.integration.edit.entryPoint({ + ...p, + ...s, + }) + } /> ); @@ -114,6 +122,14 @@ const saveIntegrationPage = ( /> ); +const apiProviderOperationsPage = ( + resolvers.list()} + rootHref={(p, s) => resolvers.integration.edit.entryPoint({ ...p, ...s })} + getFlowHref={(p, s) => resolvers.integration.edit.index({ ...p, ...s })} + /> +); + /** * Entry point for the integration editor app. This is shown when an user clicks * on the "Edit" button for any existing integration. @@ -137,32 +153,9 @@ export const IntegrationEditorApp: React.FunctionComponent = () => { return ( {t => ( - > + > {({ flowId }, { integration }) => ( - - - {t('shared:Integrations')} - - - {integration.name} - - {t('integrations:editor:addToIntegration')} - { children={addStepPage} /> + + {/* add step */} { const editAction: IIntegrationAction = { - href: resolvers.integration.edit.index({ + href: resolvers.integration.edit.entryPoint({ flowId: this.props.integration.flows![0].id!, integration: this.props.integration, }), diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/AddStepPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/AddStepPage.tsx index 79301a3a9ea..a4a64500e25 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/AddStepPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/AddStepPage.tsx @@ -17,29 +17,40 @@ import * as React from 'react'; import { Translation } from 'react-i18next'; import { PageTitle } from '../../../../shared'; import { IntegrationEditorStepAdder } from '../IntegrationEditorStepAdder'; -import { IBaseRouteParams, IBaseRouteState } from './interfaces'; +import { EditorBreadcrumb } from './EditorBreadcrumb'; +import { IBaseFlowRouteParams, IBaseRouteState } from './interfaces'; import { getStepHref, IGetStepHrefs } from './utils'; export interface IAddStepPageProps extends IGetStepHrefs { - cancelHref: (p: IBaseRouteParams, s: IBaseRouteState) => H.LocationDescriptor; + cancelHref: ( + p: IBaseFlowRouteParams, + s: IBaseRouteState + ) => H.LocationDescriptor; getAddMapperStepHref: ( position: number, - p: IBaseRouteParams, + p: IBaseFlowRouteParams, s: IBaseRouteState ) => H.LocationDescriptor; getAddStepHref: ( position: number, - p: IBaseRouteParams, + p: IBaseFlowRouteParams, s: IBaseRouteState ) => H.LocationDescriptor; getDeleteEdgeStepHref: ( position: number, - p: IBaseRouteParams, + p: IBaseFlowRouteParams, s: IBaseRouteState ) => H.LocationDescriptorObject; - saveHref: (p: IBaseRouteParams, s: IBaseRouteState) => H.LocationDescriptor; + saveHref: ( + p: IBaseFlowRouteParams, + s: IBaseRouteState + ) => H.LocationDescriptor; selfHref: ( - p: IBaseRouteParams, + p: IBaseFlowRouteParams, + s: IBaseRouteState + ) => H.LocationDescriptorObject; + rootHref: ( + p: IBaseFlowRouteParams, s: IBaseRouteState ) => H.LocationDescriptorObject; } @@ -116,8 +127,8 @@ export class AddStepPage extends React.Component< {t => ( <> - > - {({ flowId }, { integration }, { history }) => ( + > + {(params, state, { history }) => ( <> {this.state.showDeleteDialog && ( + {t('integrations:editor:addToIntegration')} + + } content={ this.props.getAddMapperStepHref( position, - { flowId }, - { integration } + params, + state ) } addStepHref={position => - this.props.getAddStepHref( - position, - { flowId }, - { integration } - ) + this.props.getAddStepHref(position, params, state) } configureStepHref={(position: number, step: Step) => getStepHref( step, - { flowId, position: `${position}` }, - { integration }, + { ...params, position: `${position}` }, + state, this.props ) } - flowId={flowId} - integration={integration} + flowId={params.flowId} + integration={state.integration} onDelete={onDelete} /> } - cancelHref={this.props.cancelHref( - { flowId }, - { integration } - )} - saveHref={this.props.saveHref({ flowId }, { integration })} - publishHref={this.props.saveHref( - { flowId }, - { integration } - )} + cancelHref={this.props.cancelHref(params, state)} + saveHref={this.props.saveHref(params, state)} + publishHref={this.props.saveHref(params, state)} /> )} diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorApp.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorApp.tsx index 1824ddcb851..45b57aaadca 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorApp.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorApp.tsx @@ -4,9 +4,7 @@ import { Integration } from '@syndesis/models'; import * as React from 'react'; import { EditSpecificationPage } from './apiProvider/EditSpecificationPage'; import { ReviewActionsPage } from './apiProvider/ReviewActionsPage'; -import { ReviewOperationsPage } from './apiProvider/ReviewOperationsPage'; import { SelectMethodPage } from './apiProvider/SelectMethodPage'; -import { SetInfoPage } from './apiProvider/SetInfoPage'; import { DataMapperPage } from './dataMapper/DataMapperPage'; import { EditorRoutes } from './EditorRoutes'; import { EditorSidebar } from './EditorSidebar'; @@ -14,14 +12,14 @@ import { ConfigureActionPage } from './endpoint/ConfigureActionPage'; import { DescribeDataShapePage } from './endpoint/DescribeDataShapePage'; import { SelectActionPage } from './endpoint/SelectActionPage'; import { + IBaseFlowRouteParams, + IBaseRouteState, IConfigureActionRouteParams, IConfigureActionRouteState, IDescribeDataShapeRouteParams, IDescribeDataShapeRouteState, ISelectConnectionRouteParams, ISelectConnectionRouteState, - ITemplateStepRouteParams, - ITemplateStepRouteState, stepRoutes, } from './interfaces'; import { makeEditorResolvers } from './makeEditorResolvers'; @@ -40,8 +38,9 @@ export interface IEditorApp { ) => H.LocationDescriptor; postConfigureHref: ( integration: Integration, - p: ITemplateStepRouteParams | IConfigureActionRouteParams, - s: ITemplateStepRouteState | IConfigureActionRouteState + p: IBaseFlowRouteParams, + s: IBaseRouteState, + isApiProvider?: boolean ) => H.LocationDescriptorObject; } @@ -56,8 +55,7 @@ export const EditorApp: React.FunctionComponent = ({ - appResolvers.apiProvider.editSpecification({ - step, + appResolvers.apiProvider.selectMethod({ ...params, ...state, }) @@ -228,52 +226,94 @@ export const EditorApp: React.FunctionComponent = ({ /> ); + const selectMethodPage = ( + + appResolvers.selectStep({ ...params, ...state }) + } + getReviewHref={(specification, params, state) => + appResolvers.apiProvider.reviewActions({ + specification, + ...params, + ...state, + }) + } + /> + ); + + const reviewActionsPage = ( + + appResolvers.apiProvider.selectMethod({ ...params, ...state }) + } + editHref={(params, state) => + appResolvers.apiProvider.editSpecification({ ...params, ...state }) + } + nextHref={(integration, params, state) => + postConfigureHref( + integration, + { + ...params, + ...state, + }, + state, + true + ) + } + /> + ); + + const editSpecificationPage = ( + + appResolvers.apiProvider.selectMethod({ ...params, ...state }) + } + saveHref={(params, state) => + appResolvers.apiProvider.reviewActions({ ...params, ...state }) + } + /> + ); + return ( - <> - , - reviewActionsPath: appStepRoutes.apiProvider.reviewActions, - reviewActionsChildren: , - editSpecificationPath: appStepRoutes.apiProvider.editSpecification, - editSpecificationChildren: , - setInfoPath: appStepRoutes.apiProvider.setInfo, - setInfoChildren: , - reviewOperationsPath: appStepRoutes.apiProvider.reviewOperations, - reviewOperationsChildren: , - }} - template={{ - templatePath: appStepRoutes.template, - templateChildren: templateStepPage, - }} - dataMapper={{ - mapperPath: appStepRoutes.dataMapper, - mapperChildren: dataMapperPage, - }} - basicFilter={{ - basicFilterPath: appStepRoutes.basicFilter, - basicFilterChildren: basicFilterPage, - }} - step={{ - configurePath: appStepRoutes.step, - configureChildren: configureStepPage, - }} - extension={{ - configurePath: appStepRoutes.extension, - configureChildren: configureStepPage, - }} - /> - + ); }; diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorBreadcrumb.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorBreadcrumb.tsx new file mode 100644 index 00000000000..3ee75e6000a --- /dev/null +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorBreadcrumb.tsx @@ -0,0 +1,92 @@ +import { getFlow, isIntegrationApiProvider } from '@syndesis/api'; +import * as H from '@syndesis/history'; +import { IntegrationOverview } from '@syndesis/models'; +import { + Breadcrumb, + ButtonLink, + HttpMethodColors, + OperationsDropdown, +} from '@syndesis/ui'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +export interface IApiProviderOperationProps { + description: string; +} +export const ApiProviderOperation: React.FunctionComponent< + IApiProviderOperationProps +> = ({ description }) => { + const [method, desc] = (description || '').split(' '); + return ( + <> + +  {desc} + + ); +}; + +export interface IEditorBreadcrumbProps { + currentFlowId?: string; + integration: IntegrationOverview; + rootHref: H.LocationDescriptor; + // getApiProviderEditorHref: (specification: string) => H.LocationDescriptor; +} +export const EditorBreadcrumb: React.FunctionComponent< + IEditorBreadcrumbProps +> = ({ + currentFlowId, + integration, + rootHref, + // getApiProviderEditorHref + children, +}) => { + const isApiProvider = isIntegrationApiProvider(integration); + const isMultiflow = + integration.flows && integration.flows.filter(f => f.name).length > 0; + const currentFlow = currentFlowId + ? getFlow(integration, currentFlowId) + : undefined; + + return ( + + View/Edit API Definition + + ) : ( + undefined + ) + } + > + + {integration.name} + + {currentFlow && isMultiflow && ( + + ) : ( + currentFlow.name + ) + } + > + Lorem ipsum dolor sit amet, consectetur adipisicing elit. A autem + dicta dolores dolorum ducimus esse fugiat hic illum laudantium, minima + nisi nulla omnis quidem quod, ratione sed sunt vel voluptatum! + + )} + {children} + + ); +}; diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorRoutes.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorRoutes.tsx index 5415c70646b..82105f588d1 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorRoutes.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/EditorRoutes.tsx @@ -1,10 +1,8 @@ import * as React from 'react'; import { Route, Switch } from 'react-router'; -import { EditSpecificationPage } from './apiProvider/EditSpecificationPage'; -import { ReviewActionsPage } from './apiProvider/ReviewActionsPage'; -import { ReviewOperationsPage } from './apiProvider/ReviewOperationsPage'; +import { IEditSpecificationPageProps } from './apiProvider/EditSpecificationPage'; +import { IReviewActionsPageProps } from './apiProvider/ReviewActionsPage'; import { SelectMethodPage } from './apiProvider/SelectMethodPage'; -import { SetInfoPage } from './apiProvider/SetInfoPage'; import { IDataMapperPageProps } from './dataMapper/DataMapperPage'; import { ConfigureActionPage } from './endpoint/ConfigureActionPage'; import { DescribeDataShapePage } from './endpoint/DescribeDataShapePage'; @@ -47,13 +45,9 @@ export interface IApiProviderAppProps { selectMethodPath: string; selectMethodChildren: React.ReactElement; reviewActionsPath: string; - reviewActionsChildren: React.ReactElement; + reviewActionsChildren: React.ReactElement; editSpecificationPath: string; - editSpecificationChildren: React.ReactElement; - setInfoPath: string; - setInfoChildren: React.ReactElement; - reviewOperationsPath: string; - reviewOperationsChildren: React.ReactElement; + editSpecificationChildren: React.ReactElement; } export const ApiProviderApp: React.FunctionComponent< IApiProviderAppProps @@ -75,16 +69,6 @@ export const ApiProviderApp: React.FunctionComponent< exact={true} children={props.editSpecificationChildren} /> - - ); }; @@ -199,16 +183,6 @@ export const EditorRoutes: React.FunctionComponent = props => { /> ) : null} - - - = props => { editSpecificationChildren={ props.apiProvider.editSpecificationChildren } - setInfoPath={props.apiProvider.setInfoPath} - setInfoChildren={props.apiProvider.setInfoChildren} - reviewOperationsPath={props.apiProvider.reviewOperationsPath} - reviewOperationsChildren={props.apiProvider.reviewOperationsChildren} + /> + + + + diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/OperationsPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/OperationsPage.tsx new file mode 100644 index 00000000000..ef251e1ba4f --- /dev/null +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/OperationsPage.tsx @@ -0,0 +1,205 @@ +import * as H from '@syndesis/history'; +import { Flow } from '@syndesis/models'; +import { + ApiProviderReviewOperations, + ApiProviderReviewOperationsItem, + IActiveFilter, + IFilterType, + IntegrationEditorLayout, + ISortType, +} from '@syndesis/ui'; +import { useRouteData, WithListViewToolbarHelpers } from '@syndesis/utils'; +import * as React from 'react'; +import { Translation } from 'react-i18next'; +import i18n from '../../../../i18n'; +import { PageTitle } from '../../../../shared'; +import { EditorBreadcrumb } from './EditorBreadcrumb'; +import { + IBaseFlowRouteParams, + IBaseRouteParams, + IBaseRouteState, +} from './interfaces'; + +interface IOperationFlow extends Flow { + implemented: number; + method: string; + description: string; +} + +function getFilteredAndSortedIntegrations( + flows: IOperationFlow[], + activeFilters: IActiveFilter[], + currentSortType: ISortType, + isSortAscending: boolean +) { + let filteredAndSortedFlows = flows; + activeFilters.forEach((filter: IActiveFilter) => { + const valueToLower = filter.value.toLowerCase(); + filteredAndSortedFlows = filteredAndSortedFlows.filter( + (f: IOperationFlow) => { + if (filter.id === 'name') { + return f.name.toLowerCase().includes(valueToLower); + } + if (filter.id === 'method') { + return f.method!.toLowerCase().includes(valueToLower); + } + return false; + } + ); + }); + + filteredAndSortedFlows = filteredAndSortedFlows.sort((fA, fB) => { + const left = isSortAscending ? fA : fB; + const right = isSortAscending ? fB : fA; + if (currentSortType.id === 'name') { + return left.name.localeCompare(right.name); + } else if (currentSortType.id === 'method') { + return left.method.localeCompare(right.method); + } else { + return left.implemented - right.implemented; + } + }); + + return filteredAndSortedFlows; +} + +const filterByName = { + filterType: 'text', + id: 'name', + placeholder: i18n.t('integrations:toolbars:filterByOperationNamePlaceholder'), + title: i18n.t('integrations:toolbars:OperationName'), +} as IFilterType; + +const filterByMethod = { + filterType: 'text', + id: 'method', + placeholder: i18n.t('integrations:toolbars:filterByMethodPlaceholder'), + title: i18n.t('integrations:toolbars:Method'), +} as IFilterType; + +const sortByName = { + id: 'name', + isNumeric: false, + title: i18n.t('integrations:toolbars:OperationName'), +} as ISortType; + +const sortByMethod = { + id: 'method', + isNumeric: false, + title: i18n.t('integrations:toolbars:Method'), +} as ISortType; + +const sortByImplemented = { + id: 'implemented', + isNumeric: true, + title: i18n.t('integrations:toolbars:OperationImplemented'), +} as ISortType; + +const sortTypes: ISortType[] = [sortByName, sortByMethod, sortByImplemented]; + +export interface IOperationsPageProps { + rootHref: (p: IBaseRouteParams, s: IBaseRouteState) => H.LocationDescriptor; + cancelHref: (p: IBaseRouteParams, s: IBaseRouteState) => H.LocationDescriptor; + getFlowHref: ( + p: IBaseFlowRouteParams, + s: IBaseRouteState + ) => H.LocationDescriptor; +} + +/** + * This is usually the final step of the API Provider user flow. + * This page shows the operations that have been previously defined + * earlier in the user flow. + */ +export const OperationsPage: React.FunctionComponent = ({ + rootHref, + cancelHref, + getFlowHref, +}) => { + const { params, state } = useRouteData(); + const flows = state + .integration!.flows!.filter(f => f.metadata && f.metadata.excerpt) + .map(f => { + const [method, description] = (f.description || '').split(' '); + return { + ...f, + description, + implemented: f.metadata!.excerpt.startsWith('501') ? 0 : 1, + method, + } as IOperationFlow; + }); + + return ( + + {t => ( + <> + + + {t('integrations:apiProvider:reviewOperations:title')} + + } + content={ + + {helpers => { + const filteredAndSortedFlows = getFilteredAndSortedIntegrations( + flows, + helpers.activeFilters, + helpers.currentSortType, + helpers.isSortAscending + ); + return ( + + {filteredAndSortedFlows.map((f, idx) => ( + + ))} + + ); + }} + + } + cancelHref={cancelHref(params, state)} + /> + + )} + + ); +}; diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx index d6ae9878efe..df17d6a92b6 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx @@ -12,11 +12,15 @@ import i18n from '../../../../i18n'; import { PageTitle } from '../../../../shared'; import { IPostPublishRouteParams, - ISaveIntegrationForm, ISaveIntegrationRouteParams, ISaveIntegrationRouteState, } from './interfaces'; +export interface ISaveIntegrationForm { + name: string; + description?: string; +} + export interface ISaveIntegrationPageProps { cancelHref: ( p: ISaveIntegrationRouteParams, @@ -52,7 +56,7 @@ export class SaveIntegrationPage extends React.Component< ISaveIntegrationRouteParams, ISaveIntegrationRouteState >> - {({ flowId }, { integration }, { history }) => ( + {(params, state, { history }) => ( {({ deployIntegration, saveIntegration }) => { let shouldPublish = false; @@ -61,7 +65,7 @@ export class SaveIntegrationPage extends React.Component< actions: any ) => { const updatedIntegration = setIntegrationProperties( - integration, + state.integration, { description, name, @@ -107,8 +111,8 @@ export class SaveIntegrationPage extends React.Component< } else { history.push( this.props.postSaveHref( - { flowId, integrationId: savedIntegration.id! }, - { integration: savedIntegration } + { integrationId: savedIntegration.id! }, + { ...state, integration: savedIntegration } ) ); } @@ -134,8 +138,8 @@ export class SaveIntegrationPage extends React.Component< i18nRequiredProperty={'* Required field'} definition={definition} initialValue={{ - description: integration.description, - name: integration.name, + description: state.integration.description, + name: state.integration.name, }} onSave={onSave} > @@ -159,10 +163,7 @@ export class SaveIntegrationPage extends React.Component< {fields} } - cancelHref={this.props.cancelHref( - { flowId }, - { integration } - )} + cancelHref={this.props.cancelHref(params, state)} onSave={submitForm} isSaveDisabled={dirty && !isValid} isSaveLoading={isSubmitting} diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/EditSpecificationPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/EditSpecificationPage.tsx index 923c50e96c6..d93f2d705cf 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/EditSpecificationPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/EditSpecificationPage.tsx @@ -1,27 +1,75 @@ import { ApicurioAdapter } from '@syndesis/apicurio-adapter'; -import { ApiProviderEditSpecification, PageSection } from '@syndesis/ui'; +import * as H from '@syndesis/history'; +import { IframeWrapper, IntegrationEditorLayout } from '@syndesis/ui'; +import { WithRouteData } from '@syndesis/utils'; import * as React from 'react'; +import { Translation } from 'react-i18next'; import { PageTitle } from '../../../../../shared'; +import { + IApiProviderReviewActionsRouteState, + IBaseApiProviderRouteParams, +} from '../interfaces'; + +export interface IEditSpecificationPageProps { + cancelHref: ( + p: IBaseApiProviderRouteParams, + s: IApiProviderReviewActionsRouteState + ) => H.LocationDescriptor; + saveHref: ( + p: IBaseApiProviderRouteParams, + s: IApiProviderReviewActionsRouteState + ) => H.LocationDescriptor; +} /** * This is the page where you define or edit your API specification. * At the moment, we are using Apicurio as the API specification editor. */ -export class EditSpecificationPage extends React.Component { - public render() { - return ( - - - - console.log(s) - } - /> - - - ); - } -} +export const EditSpecificationPage: React.FunctionComponent< + IEditSpecificationPageProps +> = ({ cancelHref, saveHref }) => { + const [specification, setSpecification] = React.useState( + undefined + ); + const onSpecification = (newSpec: any) => { + setSpecification(JSON.stringify(newSpec.spec)); + }; + + return ( + + {t => ( + > + {(params, state) => ( + <> + + + + + } + cancelHref={cancelHref(params, state)} + saveHref={saveHref(params, { + ...state, + specification: specification || state.specification, + })} + /> + + )} + + )} + + ); +}; diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/ReviewActionsPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/ReviewActionsPage.tsx index 9c503cbc492..84bfbf47033 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/ReviewActionsPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/ReviewActionsPage.tsx @@ -1,25 +1,139 @@ -import { ApiProviderReviewActions, PageSection } from '@syndesis/ui'; +import { + useApiProviderIntegration, + useApiProviderSummary, +} from '@syndesis/api'; +import * as H from '@syndesis/history'; +import { Integration } from '@syndesis/models'; +import { + ButtonLink, + IntegrationEditorLayout, + OpenApiReviewActions, + PageLoader, + PageSection, +} from '@syndesis/ui'; +import { useRouteData, WithLoader } from '@syndesis/utils'; import * as React from 'react'; -import { PageTitle } from '../../../../../shared'; +import { Translation } from 'react-i18next'; +import { ApiError, PageTitle } from '../../../../../shared'; +import { + IApiProviderReviewActionsRouteState, + IBaseApiProviderRouteParams, +} from '../interfaces'; + +export interface IReviewActionsPageProps { + cancelHref: ( + p: IBaseApiProviderRouteParams, + s: IApiProviderReviewActionsRouteState + ) => H.LocationDescriptor; + editHref: ( + p: IBaseApiProviderRouteParams, + s: IApiProviderReviewActionsRouteState + ) => H.LocationDescriptor; + nextHref: ( + integration: Integration, + p: IBaseApiProviderRouteParams, + s: IApiProviderReviewActionsRouteState + ) => H.LocationDescriptorObject; +} /** * This is the page where a user reviews the actions that have been * extracted from the API specification previously created or provided * earlier in the API Provider editor. */ -export class ReviewActionsPage extends React.Component { - public render() { - return ( - - - -

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Blanditiis - illo, iusto nesciunt nostrum omnis pariatur rerum vero voluptates. - Accusamus aliquid corporis deleniti ea earum ipsa optio, quidem quod - ut! Placeat. -

-
- ); - } -} +export const ReviewActionsPage: React.FunctionComponent< + IReviewActionsPageProps +> = ({ cancelHref, editHref, nextHref }) => { + const [nextDisabled, setNextDisabled] = React.useState(false); + const { params, state, history } = useRouteData< + IBaseApiProviderRouteParams, + IApiProviderReviewActionsRouteState + >(); + const { apiSummary, loading, error } = useApiProviderSummary( + state.specification + ); + const getIntegration = useApiProviderIntegration(); + + const onNext = async () => { + setNextDisabled(true); + try { + const integration = await getIntegration( + apiSummary!.configuredProperties!.specification + ); + delete integration.id; + history.push(nextHref(integration, params, state)); + } catch (e) { + // todo show the error? + } + setNextDisabled(false); + }; + + return ( + + {t => ( + <> + + + } + error={error !== false} + errorChildren={} + > + {() => ( + <> + ${ + apiSummary!.actionsSummary!.totalActions + } operations`} + i18nWarningsHeading={`WARNINGS ${ + apiSummary!.warnings!.length + }`} + warningMessages={apiSummary!.warnings!.map(warning => { + return (warning as any).message; + })} + /> +
+ + Review/Edit + + + Next + +
+ + )} +
+ + } + cancelHref={cancelHref(params, state)} + /> + + )} +
+ ); +}; diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/ReviewOperationsPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/ReviewOperationsPage.tsx deleted file mode 100644 index 28ee1e335e2..00000000000 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/ReviewOperationsPage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProviderReviewOperations, PageSection } from '@syndesis/ui'; -import * as React from 'react'; -import { PageTitle } from '../../../../../shared'; - -/** - * This is usually the final step of the API Provider user flow. - * This page shows the operations that have been previously defined - * earlier in the user flow. - */ -export class ReviewOperationsPage extends React.Component { - public render() { - return ( - - - -

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Blanditiis - illo, iusto nesciunt nostrum omnis pariatur rerum vero voluptates. - Accusamus aliquid corporis deleniti ea earum ipsa optio, quidem quod - ut! Placeat. -

-
- ); - } -} diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/SelectMethodPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/SelectMethodPage.tsx index 50192314823..d04425ff9c3 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/SelectMethodPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/SelectMethodPage.tsx @@ -1,24 +1,122 @@ -import { ApiProviderSelectMethod, PageSection } from '@syndesis/ui'; +// tslint:disable:no-console +import * as H from '@syndesis/history'; +import { + IntegrationEditorLayout, + Method, + OpenApiSelectMethod, + PageSection, +} from '@syndesis/ui'; +import { WithRouteData } from '@syndesis/utils'; import * as React from 'react'; +import { Translation } from 'react-i18next'; import { PageTitle } from '../../../../../shared'; +import { + IBaseApiProviderRouteParams, + IBaseApiProviderRouteState, +} from '../interfaces'; + +export interface ISelectMethodPageProps { + cancelHref: ( + p: IBaseApiProviderRouteParams, + s: IBaseApiProviderRouteState + ) => H.LocationDescriptor; + getReviewHref: ( + specification: string, + p: IBaseApiProviderRouteParams, + s: IBaseApiProviderRouteState + ) => H.LocationDescriptorObject; +} /** * The very first page of the API Provider editor, where you decide * if you want to provide an OpenAPI Spec file via drag and drop, or * if you a URL of an OpenAPI spec */ -export class SelectMethodPage extends React.Component { +export class SelectMethodPage extends React.Component { public render() { return ( - - - - + + {t => ( + > + {(params, state, { history }) => { + const onNext = (method: Method, specification: string) => { + switch (method) { + case 'file': + case 'url': + history.push( + this.props.getReviewHref(specification, params, state) + ); + break; + case 'scratch': + break; + default: + throw new Error(`Unknown method specified: ${method}`); + } + }; + + return ( + <> + + + + + } + cancelHref={this.props.cancelHref(params, state)} + /> + + ); + }} + + )} + ); } } diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/SetInfoPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/SetInfoPage.tsx index 972dde2c6b9..775dd13d033 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/SetInfoPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/apiProvider/SetInfoPage.tsx @@ -1,4 +1,4 @@ -import { ApiProviderSetInfo, PageSection } from '@syndesis/ui'; +import { /*ApiProviderSetInfo,*/ PageSection } from '@syndesis/ui'; import * as React from 'react'; import { PageTitle } from '../../../../../shared'; @@ -10,7 +10,7 @@ export class SetInfoPage extends React.Component { return ( - + {/**/}

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Blanditiis illo, iusto nesciunt nostrum omnis pariatur rerum vero voluptates. diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/ConfigureActionPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/ConfigureActionPage.tsx index b82131cfa46..787ae93c157 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/ConfigureActionPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/ConfigureActionPage.tsx @@ -76,21 +76,12 @@ export class ConfigureActionPage extends React.Component< IConfigureActionRouteParams, IConfigureActionRouteState >> - {( - { actionId, flowId, step = '0', position }, - { - configuredProperties, - connection, - integration, - updatedIntegration, - }, - { history } - ) => { - const stepAsNumber = parseInt(step, 10); - const positionAsNumber = parseInt(position, 10); + {(params, state, { history }) => { + const stepAsNumber = parseInt(params.step, 10); + const positionAsNumber = parseInt(params.position, 10); const oldStepConfig = getStep( - integration, - flowId, + state.integration, + params.flowId, positionAsNumber ); const onUpdatedIntegration = async ({ @@ -98,14 +89,14 @@ export class ConfigureActionPage extends React.Component< moreConfigurationSteps, values, }: IOnUpdatedIntegrationProps) => { - updatedIntegration = await (this.props.mode === 'adding' && - stepAsNumber === 0 + const updatedIntegration = await (this.props.mode === + 'adding' && stepAsNumber === 0 ? addConnection : updateConnection)( - updatedIntegration || integration, - connection, + state.updatedIntegration || state.integration, + state.connection, action, - flowId, + params.flowId, positionAsNumber, values ); @@ -113,15 +104,11 @@ export class ConfigureActionPage extends React.Component< history.push( this.props.nextStepHref( { - actionId, - flowId, - position, + ...params, step: `${stepAsNumber + 1}`, }, { - configuredProperties, - connection, - integration, + ...state, updatedIntegration, } ) @@ -129,7 +116,7 @@ export class ConfigureActionPage extends React.Component< } else { const stepKind = getStep( updatedIntegration, - flowId, + params.flowId, positionAsNumber ) as StepKind; const gotoDescribeData = (direction: DataShapeDirection) => { @@ -138,13 +125,12 @@ export class ConfigureActionPage extends React.Component< true, updatedIntegration!, { + ...params, direction, - flowId, - position, }, { - connection, - integration, + connection: state.connection, + integration: state.integration, step: stepKind, updatedIntegration, } @@ -156,30 +142,22 @@ export class ConfigureActionPage extends React.Component< this.props.postConfigureHref( false, updatedIntegration!, + params, { - actionId, - flowId, - position, - step, - } as IConfigureActionRouteParams, - { - configuredProperties, - connection, - integration, - step, + ...state, updatedIntegration, - } as IConfigureActionRouteState + } ) ); }; const descriptor = stepKind.action!.descriptor!; - if (isStartStep(integration, flowId, positionAsNumber)) { + if (isStartStep(state.integration, params.flowId, positionAsNumber)) { if (requiresOutputDescribeDataShape(descriptor)) { gotoDescribeData(DataShapeDirection.OUTPUT); } else { gotoDefaultNextPage(); } - } else if (isEndStep(integration, flowId, positionAsNumber)) { + } else if (isEndStep(state.integration, params.flowId, positionAsNumber)) { if (requiresInputDescribeDataShape(descriptor)) { gotoDescribeData(DataShapeDirection.INPUT); } else { @@ -207,46 +185,32 @@ export class ConfigureActionPage extends React.Component< sidebar={this.props.sidebar({ activeIndex: positionAsNumber, activeStep: { - ...toUIStep(connection), + ...toUIStep(state.connection), icon: getConnectionIcon( process.env.PUBLIC_URL, - connection + state.connection ), }, - steps: toUIStepCollection(getSteps(integration, flowId)), + steps: toUIStepCollection( + getSteps(state.integration, params.flowId) + ), })} content={ } - cancelHref={this.props.cancelHref( - { actionId, flowId, step, position }, - { - configuredProperties, - connection, - integration, - updatedIntegration, - } - )} + cancelHref={this.props.cancelHref(params, state)} /> ); diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/DescribeDataShapePage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/DescribeDataShapePage.tsx index d50e0e48f85..0922d0a0919 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/DescribeDataShapePage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/DescribeDataShapePage.tsx @@ -58,75 +58,66 @@ export class DescribeDataShapePage extends React.Component< IDescribeDataShapeRouteParams, IDescribeDataShapeRouteState >> - {( - { direction, flowId, position }, - { connection, step, integration, updatedIntegration }, - { history } - ) => { - const positionAsNumber = parseInt(position, 10); + {(params, state, { history }) => { + const positionAsNumber = parseInt(params.position, 10); const title = - direction === DataShapeDirection.INPUT + params.direction === DataShapeDirection.INPUT ? 'Specify Input Data Type' : 'Specify Output Data Type'; - const descriptor = step.action!.descriptor!; + const descriptor = state.step.action!.descriptor!; const dataShape: DataShape = - direction === DataShapeDirection.INPUT + params.direction === DataShapeDirection.INPUT ? descriptor.inputDataShape! : descriptor.outputDataShape!; const backDescribeData = this.props.backHref( 'describeData', { + ...params, direction: DataShapeDirection.INPUT, - flowId, - position, - } as IDescribeDataShapeRouteParams, - { - connection, - integration, - step, - updatedIntegration, - } as IDescribeDataShapeRouteState + }, + state ); const backActionConfig = this.props.backHref( 'configureAction', { - actionId: step.action!.id!, - flowId, - position, + ...params, + actionId: state.step.action!.id!, step: '0', - } as IConfigureActionRouteParams, - { - connection, - integration, - updatedIntegration, - } as IConfigureActionRouteState + }, + state ); const backHref = - isMiddleStep(integration, flowId, positionAsNumber) && - direction === DataShapeDirection.OUTPUT + isMiddleStep( + state.integration, + params.flowId, + positionAsNumber + ) && params.direction === DataShapeDirection.OUTPUT ? backDescribeData : backActionConfig; const handleUpdatedDataShape = async ( newDataShape: DataShape ) => { const newDescriptor = - direction === DataShapeDirection.INPUT + params.direction === DataShapeDirection.INPUT ? { ...descriptor, inputDataShape: newDataShape } : { ...descriptor, outputDataShape: newDataShape }; - const action = { ...step.action!, descriptor: newDescriptor }; - updatedIntegration = await (this.props.mode === 'adding' + const action = { + ...state.step.action!, + descriptor: newDescriptor, + }; + const updatedIntegration = await (this.props.mode === 'adding' ? addConnection : updateConnection)( - updatedIntegration || integration, - connection, + state.updatedIntegration || state.integration, + state.connection, action, - flowId, + params.flowId, positionAsNumber, - step.configuredProperties + state.step.configuredProperties ); const stepKind = getStep( updatedIntegration, - flowId, + params.flowId, positionAsNumber ) as StepKind; const gotoDescribeData = ( @@ -137,13 +128,11 @@ export class DescribeDataShapePage extends React.Component< 'describeData', updatedIntegration!, { + ...params, direction: nextDirection, - flowId, - position, }, { - connection, - integration, + ...state, step: stepKind, updatedIntegration, } @@ -156,15 +145,13 @@ export class DescribeDataShapePage extends React.Component< 'addStep', updatedIntegration!, { + ...params, actionId: stepKind.action!.id!, - flowId, - position, step: '0', } as IConfigureActionRouteParams, { + ...state, configuredProperties: stepKind.configuredProperties, - connection, - integration, step: '0', updatedIntegration, } as IConfigureActionRouteState @@ -172,16 +159,24 @@ export class DescribeDataShapePage extends React.Component< ); }; if ( - isStartStep(updatedIntegration, flowId, positionAsNumber) + isStartStep( + updatedIntegration, + params.flowId, + positionAsNumber + ) ) { gotoDefaultNextPage(); } else if ( - isEndStep(updatedIntegration, flowId, positionAsNumber) + isEndStep( + updatedIntegration, + params.flowId, + positionAsNumber + ) ) { gotoDefaultNextPage(); } else { if ( - direction === DataShapeDirection.INPUT && + params.direction === DataShapeDirection.INPUT && requiresOutputDescribeDataShape(descriptor) ) { gotoDescribeData(DataShapeDirection.OUTPUT); @@ -201,14 +196,14 @@ export class DescribeDataShapePage extends React.Component< sidebar={this.props.sidebar({ activeIndex: positionAsNumber, activeStep: { - ...toUIStep(step.connection!), + ...toUIStep(state.step.connection!), icon: getConnectionIcon( process.env.PUBLIC_URL, - step.connection! + state.step.connection! ), }, steps: toUIStepCollection( - getSteps(integration, flowId) + getSteps(state.integration, params.flowId) ), })} content={ @@ -222,15 +217,7 @@ export class DescribeDataShapePage extends React.Component< backActionHref={backHref} /> } - cancelHref={this.props.cancelHref( - { flowId, direction, position }, - { - connection, - integration, - step, - updatedIntegration, - } - )} + cancelHref={this.props.cancelHref(params, state)} /> ); diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/SelectActionPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/SelectActionPage.tsx index e8b5511bab5..7cb8bb3a5fb 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/SelectActionPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/endpoint/SelectActionPage.tsx @@ -46,10 +46,13 @@ export class SelectActionPage extends React.Component { public render() { return ( > - {({ connectionId, flowId, position }, { connection, integration }) => { - const positionAsNumber = parseInt(position, 10); + {(params, state) => { + const positionAsNumber = parseInt(params.position, 10); return ( - + {({ data, hasData, error }) => ( { sidebar={this.props.sidebar({ activeIndex: positionAsNumber, activeStep: { - ...toUIStep(connection), + ...toUIStep(state.connection), icon: getConnectionIcon( process.env.PUBLIC_URL, - connection + state.connection ), }, steps: toUIStepCollection( - getSteps(integration, flowId) + getSteps(state.integration, params.flowId) ), })} content={ @@ -100,8 +103,8 @@ export class SelectActionPage extends React.Component { )} href={this.props.selectHref( a.id!, - { connectionId, flowId, position }, - { connection, integration } + params, + state )} > Select @@ -111,10 +114,7 @@ export class SelectActionPage extends React.Component { ))} } - cancelHref={this.props.cancelHref( - { connectionId, flowId, position }, - { connection, integration } - )} + cancelHref={this.props.cancelHref(params, state)} /> )} diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/interfaces.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/interfaces.ts similarity index 69% rename from app/ui-react/syndesis/src/modules/integrations/components/editor/interfaces.tsx rename to app/ui-react/syndesis/src/modules/integrations/components/editor/interfaces.ts index ef1b0a9d307..a58984a1cae 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/interfaces.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/interfaces.ts @@ -9,34 +9,50 @@ import { } from '@syndesis/models'; import { include } from 'named-urls'; -export interface IEditorIndex { - flowId: string; - integration: Integration; +/*********************************/ +/********* UI MODELS *************/ +/*********************************/ + +export enum DataShapeDirection { + INPUT = 'input', + OUTPUT = 'output', } -export interface IEditorSelectConnection extends IEditorIndex { - position: string; +export interface IUIStep extends StepKind { + board?: ConnectionBulletinBoard; + connector?: Connector; + icon: string; + uiStepKind: 'api-provider' | StepKind['stepKind']; + title: string; + metadata: { [key: string]: any }; + inputDataShape?: DataShape; + outputDataShape?: DataShape; } -export interface IEditorSelectAction extends IEditorSelectConnection { - connection: ConnectionOverview; +export interface IUIIntegrationStep extends IUIStep { + shape: string | undefined; + previousStepShouldDefineDataShape: boolean; + shouldAddDataMapper: boolean; + isUnclosedSplit: boolean; } -export interface IEditorConfigureAction extends IEditorSelectAction { - actionId: string; - step?: string; - updatedIntegration?: Integration; +/*********************************/ +/*********** ROUTES **************/ +/*********************************/ + +export interface IBaseRouteParams { + integrationId: string; } -export interface IEditorConfigureDataShape extends IEditorSelectAction { - step: StepKind; - direction: DataShapeDirection; +export interface IBaseFlowRouteParams extends IBaseRouteParams { + flowId: string; } -export interface IEditorConfigureStep extends IEditorIndex { - position: string; - step: StepKind; - updatedIntegration?: Integration; +export interface IBaseRouteState { + /** + * the integration object to edit + */ + integration: Integration; } /** @@ -45,8 +61,7 @@ export interface IEditorConfigureStep extends IEditorIndex { * flow. * @param step - the configuration step when configuring a multi-page connection. */ -export interface IConfigureStepRouteParams { - flowId: string; +export interface IConfigureStepRouteParams extends IBaseFlowRouteParams { position: string; } @@ -58,9 +73,8 @@ export interface IConfigureStepRouteParams { * never be set. It is used by the page itself to pass the partially configured * step when configuring a multi-page connection. */ -export interface IConfigureStepRouteState { +export interface IConfigureStepRouteState extends IBaseRouteState { step: StepKind; - integration: Integration; updatedIntegration?: Integration; } @@ -70,8 +84,7 @@ export interface IConfigureStepRouteState { * flow. * @param step - the configuration step when configuring a multi-page connection. */ -export interface IConfigureActionRouteParams { - flowId: string; +export interface IConfigureActionRouteParams extends IBaseFlowRouteParams { position: string; actionId: string; step: string; @@ -85,29 +98,21 @@ export interface IConfigureActionRouteParams { * never be set. It is used by the page itself to pass the partially configured * step when configuring a multi-page connection. */ -export interface IConfigureActionRouteState { +export interface IConfigureActionRouteState extends IBaseRouteState { connection: ConnectionOverview; - integration: Integration; updatedIntegration?: Integration; configuredProperties: { [key: string]: string }; } -export enum DataShapeDirection { - INPUT = 'input', - OUTPUT = 'output', -} - -export interface IDescribeDataShapeRouteParams { - flowId: string; +export interface IDescribeDataShapeRouteParams extends IBaseFlowRouteParams { actionId?: string; position: string; direction: DataShapeDirection; } -export interface IDescribeDataShapeRouteState { +export interface IDescribeDataShapeRouteState extends IBaseRouteState { step: StepKind; connection: ConnectionOverview; - integration: Integration; updatedIntegration?: Integration; } @@ -116,8 +121,7 @@ export interface IDescribeDataShapeRouteState { * @param position - the zero-based position for the new step in the integration * flow. */ -export interface ISelectActionRouteParams { - flowId: string; +export interface ISelectActionRouteParams extends IBaseFlowRouteParams { connectionId: string; position: string; } @@ -127,85 +131,41 @@ export interface ISelectActionRouteParams { * @param connection - the connection object selected in the previous step, used * to render the IVP. */ -export interface ISelectActionRouteState { +export interface ISelectActionRouteState extends IBaseRouteState { connection: ConnectionOverview; - integration: Integration; } /** * @param position - the zero-based position for the new step in the integration * flow. */ -export interface ISelectConnectionRouteParams { - flowId: string; +export interface ISelectConnectionRouteParams extends IBaseFlowRouteParams { position: string; } +export interface IBaseApiProviderRouteParams + extends ISelectConnectionRouteParams {} +export interface IBaseApiProviderRouteState + extends IConfigureStepRouteParams, + IBaseRouteState {} +export interface IApiProviderReviewActionsRouteState + extends IBaseApiProviderRouteState { + specification: string; +} + export interface ITemplateStepRouteParams extends IConfigureStepRouteParams {} export interface ITemplateStepRouteState extends IConfigureStepRouteState {} export interface IDataMapperRouteParams extends IConfigureStepRouteParams {} export interface IDataMapperRouteState extends IConfigureStepRouteState {} export interface IRuleFilterStepRouteParams extends IConfigureStepRouteParams {} export interface IRuleFilterStepRouteState extends IConfigureStepRouteState {} - -/** - * @param integration - the integration object coming from step 3.index, used to - * render the IVP. - */ -export interface ISelectConnectionRouteState { - integration: Integration; -} - -export interface IBaseRouteParams { +export interface ISelectConnectionRouteState extends IBaseRouteState {} +export interface IPostPublishRouteParams extends IBaseRouteParams {} +export interface ISaveIntegrationRouteParams extends IBaseRouteParams {} +export interface ISaveIntegrationRouteState extends IBaseRouteState { flowId: string; } -export interface IBaseRouteState { - /** - * the integration object to edit - */ - integration: Integration; -} - -export interface ISaveIntegrationRouteParams { - flowId: string; - integrationId?: string; -} - -export interface ISaveIntegrationForm { - name: string; - description?: string; -} - -export interface IPostPublishRouteParams { - integrationId: string; -} - -/** - * @param integration - the integration object. - */ -export interface ISaveIntegrationRouteState { - integration: Integration; -} - -export interface IUIStep extends StepKind { - board?: ConnectionBulletinBoard; - connector?: Connector; - icon: string; - uiStepKind: 'api-provider' | StepKind['stepKind']; - title: string; - metadata: { [key: string]: any }; - inputDataShape?: DataShape; - outputDataShape?: DataShape; -} - -export interface IUIIntegrationStep extends IUIStep { - shape: string | undefined; - previousStepShouldDefineDataShape: boolean; - shouldAddDataMapper: boolean; - isUnclosedSplit: boolean; -} - export const stepRoutes = { // step 1 selectStep: '', @@ -214,8 +174,6 @@ export const stepRoutes = { selectMethod: '', reviewActions: 'review-actions', editSpecification: 'edit-specification', - setInfo: 'set-info', - reviewOperations: 'review-operations', }), // if selected step kind is data mapper dataMapper: 'mapper', @@ -239,10 +197,12 @@ export const stepRoutes = { * Both the integration creator and editor share the same routes when the creator * reaches the third step in the wizard. This object is to keep them DRY. */ -export const editorRoutes = include(':flowId', { - index: 'add-step', - addStep: include(':position/add', stepRoutes), - editStep: include(':position/edit', stepRoutes), +export const editorRoutes = include('editor', { + index: ':flowId/add-step', + operations: 'operations', + addStep: include(':flowId/:position/add', stepRoutes), + editStep: include(':flowId/:position/edit', stepRoutes), saveAndPublish: 'save', + entryPoint: '', root: '', }); diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/makeEditorResolvers.ts b/app/ui-react/syndesis/src/modules/integrations/components/editor/makeEditorResolvers.ts index 5bf7bde44bf..5a35ff60246 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/makeEditorResolvers.ts +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/makeEditorResolvers.ts @@ -1,8 +1,14 @@ /* tslint:disable:object-literal-sort-keys no-empty-interface */ import { getStep } from '@syndesis/api'; +import { ConnectionOverview, Integration, StepKind } from '@syndesis/models'; + import { makeResolver, makeResolverNoParams } from '@syndesis/utils'; import { configureIndexMapper } from '../../resolvers'; import { + DataShapeDirection, + IApiProviderReviewActionsRouteState, + IBaseApiProviderRouteParams, + IBaseApiProviderRouteState, IConfigureActionRouteParams, IConfigureActionRouteState, IConfigureStepRouteParams, @@ -11,11 +17,6 @@ import { IDataMapperRouteState, IDescribeDataShapeRouteParams, IDescribeDataShapeRouteState, - IEditorConfigureAction, - IEditorConfigureDataShape, - IEditorConfigureStep, - IEditorSelectAction, - IEditorSelectConnection, IRuleFilterStepRouteParams, IRuleFilterStepRouteState, ISelectActionRouteParams, @@ -27,6 +28,39 @@ import { stepRoutes, } from './interfaces'; +export interface IEditorBase { + integration: Integration; +} + +export interface IEditorIndex extends IEditorBase { + flowId: string; +} + +export interface IEditorSelectConnection extends IEditorIndex { + position: string; +} + +export interface IEditorSelectAction extends IEditorSelectConnection { + connection: ConnectionOverview; +} + +export interface IEditorConfigureAction extends IEditorSelectAction { + actionId: string; + step?: string; + updatedIntegration?: Integration; +} + +export interface IEditorConfigureDataShape extends IEditorSelectAction { + step: StepKind; + direction: DataShapeDirection; +} + +export interface IEditorConfigureStep extends IEditorIndex { + position: string; + step: StepKind; + updatedIntegration?: Integration; +} + export const configureSelectConnectionMapper = ({ position, ...rest @@ -162,6 +196,38 @@ export const configureConfigureDataMapperMapper = ({ }; }; +export interface IApiProviderConfigureStep extends IEditorSelectConnection {} +export interface IApiProviderReviewStep extends IEditorSelectConnection { + specification: string; +} + +export const apiProviderMapper = (data: IApiProviderConfigureStep) => { + const { params, state } = configureIndexMapper(data); + return { + params: { + ...params, + position: '0', + } as IBaseApiProviderRouteParams, + state: state as IBaseApiProviderRouteState, + }; +}; + +export const apiProviderReviewActionsMapper = ({ + specification, + ...rest +}: IApiProviderReviewStep) => { + const { params, state } = apiProviderMapper(rest); + return { + params: { + ...params, + } as IBaseApiProviderRouteParams, + state: { + ...state, + specification, + } as IApiProviderReviewActionsRouteState, + }; +}; + // export type RouteResolver = { // [K in keyof T]: T[K] extends string ? any : RouteResolver // }; @@ -192,14 +258,20 @@ export function makeEditorResolvers(esr: typeof stepRoutes) { }, apiProvider: { editSpecification: makeResolver< - IEditorConfigureStep, - IConfigureStepRouteParams, - IConfigureStepRouteState - >(esr.apiProvider.editSpecification, configureConfigureStepMapper), - selectMethod: makeResolverNoParams('todo select method'), - reviewActions: makeResolverNoParams('todo review actions'), - setInfo: makeResolverNoParams('todo set info'), - reviewOperations: makeResolverNoParams('todo review operations'), + IApiProviderReviewStep, + IBaseApiProviderRouteParams, + IApiProviderReviewActionsRouteState + >(esr.apiProvider.editSpecification, apiProviderReviewActionsMapper), + selectMethod: makeResolver< + IApiProviderConfigureStep, + IBaseApiProviderRouteParams, + IBaseApiProviderRouteState + >(esr.apiProvider.selectMethod, apiProviderMapper), + reviewActions: makeResolver< + IApiProviderReviewStep, + IBaseApiProviderRouteParams, + IApiProviderReviewActionsRouteState + >(esr.apiProvider.reviewActions, apiProviderReviewActionsMapper), }, basicFilter: makeResolver< IEditorConfigureStep, diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/ruleFilter/RuleFilterStepPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/ruleFilter/RuleFilterStepPage.tsx index 65160f90e05..7ccdb979797 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/ruleFilter/RuleFilterStepPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/ruleFilter/RuleFilterStepPage.tsx @@ -54,17 +54,13 @@ export class RuleFilterStepPage extends React.Component< IRuleFilterStepRouteParams, IRuleFilterStepRouteState >> - {( - { flowId, position }, - { step, integration, updatedIntegration }, - { history } - ) => { - const positionAsNumber = parseInt(position, 10); + {(params, state, { history }) => { + const positionAsNumber = parseInt(params.position, 10); let dataShape = {} as DataShape; try { const prevStep = getPreviousIntegrationStepWithDataShape( - integration, - flowId, + state.integration, + params.flowId, positionAsNumber ); dataShape = @@ -76,25 +72,20 @@ export class RuleFilterStepPage extends React.Component< const handleSubmitForm = async ({ values, }: IOnUpdatedIntegrationProps) => { - updatedIntegration = await (this.props.mode === 'adding' + const updatedIntegration = await (this.props.mode === 'adding' ? addStep : updateStep)( - updatedIntegration || integration, - step, - flowId, + state.updatedIntegration || state.integration, + state.step, + params.flowId, positionAsNumber, values ); history.push( - this.props.postConfigureHref( + this.props.postConfigureHref(updatedIntegration, params, { + ...state, updatedIntegration, - { flowId, position }, - { - integration, - step, - updatedIntegration, - } - ) + }) ); }; return ( @@ -107,9 +98,12 @@ export class RuleFilterStepPage extends React.Component< } sidebar={this.props.sidebar({ activeIndex: positionAsNumber, - activeStep: toUIStep(step), + activeStep: toUIStep(state.step), steps: toUIStepCollection( - getSteps(updatedIntegration || integration, flowId) + getSteps( + state.updatedIntegration || state.integration, + params.flowId + ) ), })} content={ @@ -127,7 +121,7 @@ export class RuleFilterStepPage extends React.Component< > {() => ( @@ -148,14 +142,7 @@ export class RuleFilterStepPage extends React.Component< )} } - cancelHref={this.props.cancelHref( - { flowId, position }, - { - integration, - step, - updatedIntegration, - } - )} + cancelHref={this.props.cancelHref(params, state)} /> ); diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/step/ConfigureStepPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/step/ConfigureStepPage.tsx index d2398040198..b1f02cef44d 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/step/ConfigureStepPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/step/ConfigureStepPage.tsx @@ -52,41 +52,32 @@ export class ConfigureStepPage extends React.Component< {({ addStep, updateStep }) => ( > - {( - { flowId, position }, - { step, integration, updatedIntegration }, - { history } - ) => { - const positionAsNumber = parseInt(position, 10); + {(params, state, { history }) => { + const positionAsNumber = parseInt(params.position, 10); const onUpdatedIntegration = async ({ values, }: IOnUpdatedIntegrationProps) => { - updatedIntegration = await (this.props.mode === 'adding' + const updatedIntegration = await (this.props.mode === 'adding' ? addStep : updateStep)( - updatedIntegration || integration, - step, - flowId, + state.updatedIntegration || state.integration, + state.step, + params.flowId, positionAsNumber, values ); history.push( - this.props.postConfigureHref( + this.props.postConfigureHref(updatedIntegration, params, { + ...state, updatedIntegration, - { flowId, position }, - { - integration, - step, - updatedIntegration, - } - ) + }) ); }; return ( {({ form }) => ( @@ -99,20 +90,16 @@ export class ConfigureStepPage extends React.Component< } sidebar={this.props.sidebar({ activeIndex: positionAsNumber, - activeStep: toUIStep(step), + activeStep: toUIStep(state.step), steps: toUIStepCollection( - getSteps(updatedIntegration || integration, flowId) + getSteps( + state.updatedIntegration || state.integration, + params.flowId + ) ), })} content={form} - cancelHref={this.props.cancelHref( - { flowId, position }, - { - integration, - step, - updatedIntegration, - } - )} + cancelHref={this.props.cancelHref(params, state)} /> )} diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/template/TemplateStepPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/template/TemplateStepPage.tsx index ccc0d501ec2..3651ff52d81 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/template/TemplateStepPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/template/TemplateStepPage.tsx @@ -42,12 +42,8 @@ export class TemplateStepPage extends React.Component { {({ addStep, updateStep }) => ( > - {( - { flowId, position }, - { step, integration, updatedIntegration }, - { history } - ) => { - const positionAsNumber = parseInt(position, 10); + {(params, state, { history }) => { + const positionAsNumber = parseInt(params.position, 10); let isValid = true; const handleUpdateLinting = ( unsortedAnnotations: any[], @@ -59,28 +55,28 @@ export class TemplateStepPage extends React.Component { action, values, }: StringMap) => { - updatedIntegration = await (this.props.mode === 'adding' + const updatedIntegration = await (this.props.mode === 'adding' ? addStep : updateStep)( - updatedIntegration || integration, - setActionOnStep(step as Step, action, TEMPLATE) as StepKind, - flowId, + state.updatedIntegration || state.integration, + setActionOnStep( + state.step as Step, + action, + TEMPLATE + ) as StepKind, + params.flowId, positionAsNumber, values ); history.push( - this.props.postConfigureHref( + this.props.postConfigureHref(updatedIntegration, params, { + ...state, updatedIntegration, - { flowId, position }, - { - integration, - step, - updatedIntegration, - } - ) + }) ); }; - const configuredProperties = step.configuredProperties || {}; + const configuredProperties = + state.step.configuredProperties || {}; const language = configuredProperties.language || TemplateType.Mustache; const template = configuredProperties.template || ''; @@ -94,9 +90,12 @@ export class TemplateStepPage extends React.Component { } sidebar={this.props.sidebar({ activeIndex: positionAsNumber, - activeStep: toUIStep(step), + activeStep: toUIStep(state.step), steps: toUIStepCollection( - getSteps(updatedIntegration || integration, flowId) + getSteps( + state.updatedIntegration || state.integration, + params.flowId + ) ), })} content={ @@ -117,14 +116,7 @@ export class TemplateStepPage extends React.Component { )} } - cancelHref={this.props.cancelHref( - { flowId, position }, - { - integration, - step, - updatedIntegration, - } - )} + cancelHref={this.props.cancelHref(params, state)} /> ); diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/utils.ts b/app/ui-react/syndesis/src/modules/integrations/components/editor/utils.ts index 5e86eba9296..9c35442fa69 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/utils.ts +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/utils.ts @@ -43,7 +43,11 @@ type StepKindHrefCallback = ( ) => H.LocationDescriptorObject; export function getStepKind(step: Step): IUIStep['uiStepKind'] { - if (step.connection && step.connection.id === API_PROVIDER) { + if ( + step.connection && + step.connection.id === API_PROVIDER && + !(step.metadata || {}).configured + ) { return API_PROVIDER; } return step.stepKind; diff --git a/app/ui-react/syndesis/src/modules/integrations/locales/integrations-translations.en.json b/app/ui-react/syndesis/src/modules/integrations/locales/integrations-translations.en.json index 5069f550127..39077f09935 100644 --- a/app/ui-react/syndesis/src/modules/integrations/locales/integrations-translations.en.json +++ b/app/ui-react/syndesis/src/modules/integrations/locales/integrations-translations.en.json @@ -76,6 +76,44 @@ "confirmDeleteStepDialogTitle": "Confirm Delete", "saveOrAddStep": "Save or Add Step" }, + "apiProvider": { + "editSpecification": { + "title": "Provide API Definition", + "description": "This description has not yet been actually defined, please send help." + }, + "reviewActions": { + "btnReviewEdit": "Review/Edit", + "name": "Name", + "operations": "operations", + "sectionApiDefinition": "API DEFINITION", + "sectionImported": "IMPORTED", + "sectionWarnings": "WARNINGS", + "title": "Review Actions", + "description": "This description has not yet been actually defined, please send help." + }, + "reviewOperations": { + "title": "Operations", + "description": "An integration can have multiple flows. Select an operation to start creating a flow." + }, + "selectMethod": { + "description": "Execute this integration when a client invokes an operation defined by this API.", + "dndUploadFailedMessage": " could not be uploaded", + "dndUploadSuccessMessage": "Process file ", + "dndFileExtensions": ".json,.yaml,.yml", + "dndHelpMessage": "Accepted file type: .json, .yaml, and .yml", + "dndInstructions": "Drag 'n' drop a file here, or click to select a file using a file chooser dialog.", + "dndNoFileSelectedLabel": "No file selected", + "dndSelectedFileLabel": "Selected file:", + "methodFromFile": "Upload an OpenAPI file", + "methodFromScratch": "Create from scratch", + "methodFromUrl": "Use a URL", + "title": "Start integration with an API call", + "urlNote": "* Note: After uploading this document, Syndesis does not automatically obtain any updates to it." + }, + "setInfo": { + "title": "Give this integration a name" + } + }, "ReplaceDraft": "Replace Draft", "ReplaceDraftModalMessage": "Are you sure you want to to replace the current draft for the \"{{name}}\" integration?", "ReplaceDraftMOdalTitle": "Replace Draft?", @@ -112,5 +150,12 @@ "templater-import-review": "{{import}} review:" }, "unsavedChangesTitle": "Unsaved Changes", - "unsavedChangesMessage": "Are you sure you want to exit editing the integration?" + "unsavedChangesMessage": "Are you sure you want to exit editing the integration?", + "toolbars": { + "filterByOperationNamePlaceholder": "Filter by operation name", + "filterByMethodPlaceholder": "Filter by method", + "Method": "Method", + "OperationImplemented": "Operation implemented", + "OperationName": "Operation name" + } } diff --git a/app/ui-react/syndesis/src/modules/integrations/pages/IntegrationsPage.tsx b/app/ui-react/syndesis/src/modules/integrations/pages/IntegrationsPage.tsx index 27f515a6740..1b7963d207a 100644 --- a/app/ui-react/syndesis/src/modules/integrations/pages/IntegrationsPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/pages/IntegrationsPage.tsx @@ -17,7 +17,7 @@ import resolvers from '../resolvers'; function getFilteredAndSortedIntegrations( integrations: IntegrationWithMonitoring[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { let filteredAndSortedIntegrations = integrations; @@ -25,10 +25,10 @@ function getFilteredAndSortedIntegrations( const valueToLower = filter.value.toLowerCase(); filteredAndSortedIntegrations = filteredAndSortedIntegrations.filter( (mi: IntegrationWithMonitoring) => { - if (filter.title === 'Name') { + if (filter.id === 'name') { return mi.integration.name.toLowerCase().includes(valueToLower); } - if (filter.title === 'Connection') { + if (filter.id === 'connection') { const connectionNames = mi.integration!.flows!.reduce( (acc, flow) => [ ...acc, @@ -52,7 +52,7 @@ function getFilteredAndSortedIntegrations( (miA, miB) => { const left = isSortAscending ? miA : miB; const right = isSortAscending ? miB : miA; - if (currentSortType === 'Name') { + if (currentSortType.id === 'name') { return left.integration.name.localeCompare(right.integration.name); } return left.integration!.currentState!.localeCompare( diff --git a/app/ui-react/syndesis/src/modules/integrations/pages/cicd/ManageCiCdPage.tsx b/app/ui-react/syndesis/src/modules/integrations/pages/cicd/ManageCiCdPage.tsx index e70a9edcf56..e91d314c783 100644 --- a/app/ui-react/syndesis/src/modules/integrations/pages/cicd/ManageCiCdPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/pages/cicd/ManageCiCdPage.tsx @@ -23,7 +23,7 @@ import resolvers from '../../resolvers'; function getFilteredAndSortedEnvironments( environments: string[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { let answer = environments; diff --git a/app/ui-react/syndesis/src/modules/integrations/resolvers.ts b/app/ui-react/syndesis/src/modules/integrations/resolvers.ts index 307fae68689..587977e2b12 100644 --- a/app/ui-react/syndesis/src/modules/integrations/resolvers.ts +++ b/app/ui-react/syndesis/src/modules/integrations/resolvers.ts @@ -1,5 +1,5 @@ /* tslint:disable:object-literal-sort-keys no-empty-interface */ -import { getEmptyIntegration } from '@syndesis/api'; +import { getEmptyIntegration, isIntegrationApiProvider } from '@syndesis/api'; import { IIntegrationOverviewWithDraft } from '@syndesis/models'; import { makeResolver, @@ -7,15 +7,19 @@ import { makeResolverNoParamsWithDefaults, } from '@syndesis/utils'; import { + IBaseFlowRouteParams, IBaseRouteParams, IBaseRouteState, - IEditorIndex, ISaveIntegrationRouteParams, ISaveIntegrationRouteState, ISelectConnectionRouteParams, ISelectConnectionRouteState, } from './components/editor/interfaces'; -import { makeEditorResolvers } from './components/editor/makeEditorResolvers'; +import { + IEditorBase, + IEditorIndex, + makeEditorResolvers, +} from './components/editor/makeEditorResolvers'; import { IDetailsRouteParams, IDetailsRouteState, @@ -28,7 +32,54 @@ export const configureIndexMapper = ({ }: IEditorIndex) => ({ params: { flowId: flowId ? flowId : integration.flows![0].id!, - ...(integration && integration.id ? { integrationId: integration.id } : {}), + integrationId: integration.id!, + } as IBaseFlowRouteParams, + state: { + integration, + } as IBaseRouteState, +}); + +export const configureIndexOrApiProviderMapper = ( + indexRoute: string, + apiProviderRoute: string +) => ({ flowId, integration }: { flowId?: string } & IEditorBase) => { + return isIntegrationApiProvider(integration!) + ? { + params: { + integrationId: integration.id!, + } as IBaseFlowRouteParams, + route: apiProviderRoute, + state: { + integration, + } as IBaseRouteState, + } + : { + params: { + flowId: flowId ? flowId : integration.flows![0].id!, + integrationId: integration.id!, + } as IBaseFlowRouteParams, + route: indexRoute, + state: { + integration, + } as IBaseRouteState, + }; +}; + +export const configureSaveMapper = ({ flowId, integration }: IEditorIndex) => ({ + params: { + integrationId: integration.id!, + } as ISaveIntegrationRouteParams, + state: { + flowId, + integration, + } as ISaveIntegrationRouteState, +}); + +export const configureApiProviderOperationsMapper = ({ + integration, +}: IEditorBase) => ({ + params: { + integrationId: integration.id, } as IBaseRouteParams, state: { integration, @@ -97,6 +148,7 @@ const resolvers = { return { params: { flowId: integration.flows![0].id!, + integrationId: integration.id!, position: '0', }, state: { @@ -108,17 +160,32 @@ const resolvers = { finish: makeEditorResolvers(routes.create.finish), configure: { root: makeResolverNoParams(routes.create.configure.root), - index: makeResolver( + entryPoint: makeResolver< + { flowId?: string } & IEditorBase, + IBaseFlowRouteParams, + IBaseRouteState + >( + routes.create.configure.index, + configureIndexOrApiProviderMapper( + routes.create.configure.index, + routes.create.configure.operations + ) + ), + index: makeResolver( routes.create.configure.index, configureIndexMapper ), + operations: makeResolver( + routes.create.configure.operations, + configureApiProviderOperationsMapper + ), addStep: makeEditorResolvers(routes.create.configure.addStep), editStep: makeEditorResolvers(routes.create.configure.editStep), saveAndPublish: makeResolver< IEditorIndex, ISaveIntegrationRouteParams, ISaveIntegrationRouteState - >(routes.create.configure.saveAndPublish, configureIndexMapper), + >(routes.create.configure.saveAndPublish, configureSaveMapper), }, }, integration: { @@ -126,21 +193,36 @@ const resolvers = { activity: integrationActivityResolver, details: integrationDetailsResolver, edit: { - root: makeResolver( + root: makeResolver( routes.integration.edit.root, configureIndexMapper ), - index: makeResolver( + entryPoint: makeResolver< + { flowId?: string } & IEditorBase, + IBaseFlowRouteParams, + IBaseRouteState + >( + routes.integration.edit.index, + configureIndexOrApiProviderMapper( + routes.integration.edit.index, + routes.integration.edit.operations + ) + ), + index: makeResolver( routes.integration.edit.index, configureIndexMapper ), + operations: makeResolver( + routes.integration.edit.operations, + configureApiProviderOperationsMapper + ), addStep: makeEditorResolvers(routes.integration.edit.addStep), editStep: makeEditorResolvers(routes.integration.edit.editStep), saveAndPublish: makeResolver< IEditorIndex, ISaveIntegrationRouteParams, ISaveIntegrationRouteState - >(routes.integration.edit.saveAndPublish, configureIndexMapper), + >(routes.integration.edit.saveAndPublish, configureSaveMapper), }, metrics: metricsResolver, }, diff --git a/app/ui-react/syndesis/src/modules/integrations/routes.ts b/app/ui-react/syndesis/src/modules/integrations/routes.ts index ed7e5684236..86e70afd0c1 100644 --- a/app/ui-react/syndesis/src/modules/integrations/routes.ts +++ b/app/ui-react/syndesis/src/modules/integrations/routes.ts @@ -7,9 +7,9 @@ export default include('/integrations', { manageCicd: include('manageCicd', { root: '' }), import: 'import', create: include('create', { - start: include('start/:flowId/:position', stepRoutes), - finish: include('finish/:flowId/:position', stepRoutes), - configure: include('configure', editorRoutes), + start: include(':integrationId/start/:flowId/:position', stepRoutes), + finish: include(':integrationId/finish/:flowId/:position', stepRoutes), + configure: include(':integrationId/configure', editorRoutes), root: '', }), integration: include(':integrationId', { diff --git a/app/ui-react/syndesis/src/modules/settings/pages/OAuthAppsPage.tsx b/app/ui-react/syndesis/src/modules/settings/pages/OAuthAppsPage.tsx index b76090d63f9..d29e6c1de7f 100644 --- a/app/ui-react/syndesis/src/modules/settings/pages/OAuthAppsPage.tsx +++ b/app/ui-react/syndesis/src/modules/settings/pages/OAuthAppsPage.tsx @@ -33,7 +33,7 @@ import { ApiError, PageTitle } from '../../../shared'; function getFilteredAndSortedOAuthApps( oauthApps: OAuthApp[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) { let answer = oauthApps; diff --git a/app/ui-react/syndesis/src/modules/support/components/SelectiveIntegrationList.tsx b/app/ui-react/syndesis/src/modules/support/components/SelectiveIntegrationList.tsx index bb65127ba56..ac3394bd06c 100644 --- a/app/ui-react/syndesis/src/modules/support/components/SelectiveIntegrationList.tsx +++ b/app/ui-react/syndesis/src/modules/support/components/SelectiveIntegrationList.tsx @@ -36,7 +36,7 @@ export const SelectiveIntegrationList: React.FunctionComponent< const getFilteredAndSortedIntegrations = ( integrations: IntegrationOverview[], activeFilters: IActiveFilter[], - currentSortType: string, + currentSortType: ISortType, isSortAscending: boolean ) => { let filteredAndSortedIntegrations = integrations; @@ -45,7 +45,7 @@ export const SelectiveIntegrationList: React.FunctionComponent< const valueToLower = filter.value.toLowerCase(); filteredAndSortedIntegrations = filteredAndSortedIntegrations.filter( (si: IntegrationOverview) => { - if (filter.title === 'Name') { + if (filter.id === 'name') { return si.name.toLowerCase().includes(valueToLower); } return false; @@ -57,7 +57,7 @@ export const SelectiveIntegrationList: React.FunctionComponent< (siA, siB) => { const left = isSortAscending ? siA : siB; const right = isSortAscending ? siB : siA; - if (currentSortType === 'Name') { + if (currentSortType.id === 'name') { return left.name.localeCompare(right.name); } return left.currentState!.localeCompare(right.currentState!); diff --git a/app/ui-react/yarn.lock b/app/ui-react/yarn.lock index 0d6796a30ba..3ad6f60a9bb 100644 --- a/app/ui-react/yarn.lock +++ b/app/ui-react/yarn.lock @@ -21327,6 +21327,18 @@ url@0.11.0, url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-force-update@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/use-force-update/-/use-force-update-1.0.5.tgz#9b7192f73f6cb90d592b225858c1562719d7c184" + integrity sha512-wUWnEzE9ezjKky/V/pyEVVjfXbOsIqshrPeD6uSaA30zzdgW6XmvuJkKB64LP6qAEZvSuEDvvaVCjrmF717uAA== + +use-react-router@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/use-react-router/-/use-react-router-1.0.7.tgz#04216066d87e45040309f24d2fd5e9f28308b3e2" + integrity sha512-gdqIHEO28E+qDJQ+tOMGQPthPib7mhLI/4MY7wtxJuaVUkosbP+FAYcHmmE7/FjYoMsuzL/bvY/25st7QHodpw== + dependencies: + use-force-update "^1.0.5" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"