diff --git a/app/ui-react/packages/api/src/WithActivities.tsx b/app/ui-react/packages/api/src/WithActivities.tsx new file mode 100644 index 00000000000..8e3bb940a1c --- /dev/null +++ b/app/ui-react/packages/api/src/WithActivities.tsx @@ -0,0 +1,47 @@ +import { Activity, IntegrationDeployment } from '@syndesis/models'; +import * as React from 'react'; +import { SyndesisFetch } from './SyndesisFetch'; + +export interface IIntegrationDeploymentResponse { + items: IntegrationDeployment[]; + totalCount: number; +} + +export interface IActivitiesAndDeploymentsChildrenProps { + activities: Activity[]; + deployments: IntegrationDeployment[]; + fetchDeployments: () => Promise; + fetchActivities: () => Promise; +} + +export interface IWithActivitiesProps { + integrationId: string; + children(props: IActivitiesAndDeploymentsChildrenProps): any; +} + +export class WithActivities extends React.Component { + public render() { + return ( + + url={`/integrations/${this.props.integrationId}/deployments`} + defaultValue={{ items: [], totalCount: 0 }} + > + {({ read: fetchDeployments, response: deployments }) => ( + + url={`/activity/integrations/${this.props.integrationId}`} + defaultValue={[]} + > + {({ read: fetchActivities, response: activities }) => { + return this.props.children({ + activities: activities.data, + deployments: deployments.data.items, + fetchActivities, + fetchDeployments, + }); + }} + + )} + + ); + } +} diff --git a/app/ui-react/packages/api/src/WithMonitoredIntegration.tsx b/app/ui-react/packages/api/src/WithMonitoredIntegration.tsx new file mode 100644 index 00000000000..3c8f4465d81 --- /dev/null +++ b/app/ui-react/packages/api/src/WithMonitoredIntegration.tsx @@ -0,0 +1,93 @@ +import { + IIntegrationOverviewWithDraft, + IntegrationMonitoring, + IntegrationWithMonitoring, +} from '@syndesis/models'; +import * as React from 'react'; +import { IFetchState } from './Fetch'; +import { ServerEventsContext } from './ServerEventsContext'; +import { SyndesisFetch } from './SyndesisFetch'; +import { WithChangeListener } from './WithChangeListener'; +import { WithIntegration } from './WithIntegration'; +import { IChangeEvent } from './WithServerEvents'; + +export interface IWithMonitoredIntegrationProps { + integrationId: string; + disableUpdates?: boolean; + children(props: IFetchState): any; +} + +/** + * A component that fetches the integration with the specified identifier. + * @see [integrationId]{@link IWithIntegrationProps#integrationId} + */ +export class WithMonitoredIntegration extends React.Component< + IWithMonitoredIntegrationProps +> { + public constructor(props: IWithMonitoredIntegrationProps) { + super(props); + this.changeFilter = this.changeFilter.bind(this); + } + public changeFilter(change: IChangeEvent) { + return ( + change.kind.startsWith('integration') && + change.id.startsWith(this.props.integrationId) + ); + } + public getMonitoredIntegration( + integration: IIntegrationOverviewWithDraft, + response: IFetchState + ) { + return { + integration, + monitoring: response.data.find( + (o: IntegrationMonitoring) => o.integrationId === integration.id + ), + }; + } + public render() { + return ( + + {({ data: integration, ...props }) => ( + + url={'/monitoring/integrations'} + defaultValue={[]} + > + {({ read, response }) => { + if (this.props.disableUpdates) { + const data = this.getMonitoredIntegration( + integration, + response + ); + return this.props.children({ ...props, data }); + } + return ( + + {({ registerChangeListener, unregisterChangeListener }) => ( + + {() => { + const data = this.getMonitoredIntegration( + integration, + response + ); + return this.props.children({ ...props, data }); + }} + + )} + + ); + }} + + )} + + ); + } +} diff --git a/app/ui-react/packages/api/src/index.ts b/app/ui-react/packages/api/src/index.ts index a63db0472b0..f67d641125e 100644 --- a/app/ui-react/packages/api/src/index.ts +++ b/app/ui-react/packages/api/src/index.ts @@ -6,6 +6,7 @@ export * from './Fetch'; export * from './ServerEventsContext'; export * from './Stream'; export * from './WithIntegrationHelpers'; +export * from './WithActivities'; export * from './WithApiConnectors'; export * from './WithApiVersion'; export * from './WithEnvironments'; @@ -17,6 +18,7 @@ export * from './WithExtensions'; export * from './WithIntegration'; export * from './WithIntegrations'; export * from './WithIntegrationTags'; +export * from './WithMonitoredIntegration'; export * from './WithMonitoredIntegrations'; export * from './WithIntegrationsMetrics'; export * from './WithConnections'; diff --git a/app/ui-react/packages/models/src/extra.d.ts b/app/ui-react/packages/models/src/extra.d.ts index 9624af9192c..4ee60ad20d0 100644 --- a/app/ui-react/packages/models/src/extra.d.ts +++ b/app/ui-react/packages/models/src/extra.d.ts @@ -15,6 +15,29 @@ import { * */ +// this is for the logging backend +export interface Activity { + logts?: string; + at: number; + pod: string; + ver: string; + status: string; + failed: boolean; + steps?: ActivityStep[]; + metadata?: any; +} + +export interface ActivityStep extends Step { + name: string; + isFailed: boolean; + at: number; + duration?: number; + failure?: string; + messages?: string[]; + output?: string; + events?: any; +} + export interface IApiVersion { version: string; 'commit-id': string; diff --git a/app/ui-react/packages/ui/src/Integration/IntegrationDetailInfo.tsx b/app/ui-react/packages/ui/src/Integration/IntegrationDetailInfo.tsx index ac8975b8641..99310046f5d 100644 --- a/app/ui-react/packages/ui/src/Integration/IntegrationDetailInfo.tsx +++ b/app/ui-react/packages/ui/src/Integration/IntegrationDetailInfo.tsx @@ -3,10 +3,22 @@ import * as React from 'react'; import { IntegrationDetailEditableName } from './IntegrationDetailEditableName'; import './IntegrationDetailInfo.css'; +import { IntegrationStatusDetail } from './IntegrationStatusDetail'; +import { IntegrationState } from './models'; export interface IIntegrationDetailInfoProps { name?: string; version?: number; + currentState: IntegrationState; + targetState: IntegrationState; + monitoringValue?: string; + monitoringCurrentStep?: number; + monitoringTotalSteps?: number; + monitoringLogUrl?: string; + i18nProgressPending: string; + i18nProgressStarting: string; + i18nProgressStopping: string; + i18nLogUrlText: string; } export class IntegrationDetailInfo extends React.PureComponent< @@ -16,14 +28,27 @@ export class IntegrationDetailInfo extends React.PureComponent< return (
- {this.props.version ? ( - <> - -  Published version {this.props.version} - - ) : ( - 'Stopped' - )} + <> + {this.props.currentState === 'Pending' && ( + + )} + {this.props.currentState === 'Published' && this.props.version && ( + <> + +  Published version {this.props.version} + + )} +
); } diff --git a/app/ui-react/syndesis/src/modules/integrations/components/IntegrationDetailHeader.tsx b/app/ui-react/syndesis/src/modules/integrations/components/IntegrationDetailHeader.tsx new file mode 100644 index 00000000000..15f433bb7be --- /dev/null +++ b/app/ui-react/syndesis/src/modules/integrations/components/IntegrationDetailHeader.tsx @@ -0,0 +1,89 @@ +import { canActivate, canDeactivate } from '@syndesis/api'; +import { + IntegrationMonitoring, + IntegrationWithMonitoring, +} from '@syndesis/models'; +import { + IMenuActions, + IntegrationDetailBreadcrumb, + IntegrationDetailInfo, +} from '@syndesis/ui'; +import * as React from 'react'; +import { Translation } from 'react-i18next'; +import resolvers from '../../resolvers'; +import { IntegrationDetailNavBar } from '../shared'; + +export interface IIntegrationDetailHeaderProps { + data: IntegrationWithMonitoring; + startAction: IMenuActions; + stopAction: IMenuActions; + deleteAction: IMenuActions; + ciCdAction: IMenuActions; + editAction: IMenuActions; + exportAction: IMenuActions; + getPodLogUrl: ( + monitoring: IntegrationMonitoring | undefined + ) => string | undefined; +} + +export const IntegrationDetailHeader: React.FunctionComponent< + IIntegrationDetailHeaderProps +> = (props: IIntegrationDetailHeaderProps) => { + const breadcrumbMenuActions: IMenuActions[] = []; + if (canActivate(props.data.integration)) { + breadcrumbMenuActions.push(props.startAction); + } + if (canDeactivate(props.data.integration)) { + breadcrumbMenuActions.push(props.stopAction); + } + breadcrumbMenuActions.push(props.deleteAction); + breadcrumbMenuActions.push(props.ciCdAction); + + return ( + + {t => ( + <> + + + + + + )} + + ); +}; diff --git a/app/ui-react/syndesis/src/modules/integrations/components/index.ts b/app/ui-react/syndesis/src/modules/integrations/components/index.ts index a4b38029c72..e2ff6f363bc 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/index.ts +++ b/app/ui-react/syndesis/src/modules/integrations/components/index.ts @@ -1,5 +1,6 @@ export * from './Integrations'; export * from './IntegrationCreatorBreadcrumbs'; +export * from './IntegrationDetailHeader'; export * from './IntegrationDetailSteps'; export * from './IntegrationEditorBreadcrumbs'; export * from './IntegrationEditorSidebar'; diff --git a/app/ui-react/syndesis/src/modules/integrations/pages/detail/ActivityPage.tsx b/app/ui-react/syndesis/src/modules/integrations/pages/detail/ActivityPage.tsx index 85abc1f318c..46f8e26702f 100644 --- a/app/ui-react/syndesis/src/modules/integrations/pages/detail/ActivityPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/pages/detail/ActivityPage.tsx @@ -1,8 +1,16 @@ +import { WithMonitoredIntegration } from '@syndesis/api'; import { Integration } from '@syndesis/models'; -import { WithRouteData } from '@syndesis/utils'; +import { IntegrationDetailActivity, Loader } from '@syndesis/ui'; +import { WithLoader, WithRouteData } from '@syndesis/utils'; import * as React from 'react'; import { Translation } from 'react-i18next'; -import { IntegrationDetailNavBar } from '../../shared'; +import { AppContext } from '../../../../app'; +import { ApiError, PageTitle } from '../../../../shared'; +import { + IntegrationDetailHeader, + WithIntegrationActions, +} from '../../components'; +import { ActivityPageTable } from './ActivityPageTable'; /** * @integrationId - the ID of the integration for which details are being displayed. @@ -25,18 +33,84 @@ export interface IActivityPageState { export class ActivityPage extends React.Component { public render() { return ( - > - {({ integrationId }, { integration }, { history }) => { - return ( -
- - {t => } - -

This is the Integration Detail Activity page.

-
- ); - }} - + + {t => ( + + {({ getPodLogUrl }) => ( + > + {({ integrationId }, { integration }, { history }) => { + return ( + <> + + {({ data, hasData, error }) => ( + } + errorChildren={} + > + {() => ( + + {({ + ciCdAction, + editAction, + deleteAction, + exportAction, + startAction, + stopAction, + }) => { + return ( + <> + + + + } + /> + + ); + }} + + )} + + )} + + + ); + }} + + )} + + )} + ); } } diff --git a/app/ui-react/syndesis/src/modules/integrations/pages/detail/ActivityPageTable.tsx b/app/ui-react/syndesis/src/modules/integrations/pages/detail/ActivityPageTable.tsx new file mode 100644 index 00000000000..c3e243bf774 --- /dev/null +++ b/app/ui-react/syndesis/src/modules/integrations/pages/detail/ActivityPageTable.tsx @@ -0,0 +1,144 @@ +import { WithActivities } from '@syndesis/api'; +import { + Activity, + ActivityStep, + IntegrationDeployment, + Step, +} from '@syndesis/models'; +import { + IntegrationDetailActivityItem, + IntegrationDetailActivityItemSteps, +} from '@syndesis/ui'; +import * as React from 'react'; +import { Translation } from 'react-i18next'; +import i18n from '../../../../i18n'; + +export interface IActivityPagetableProps { + integrationId: string; +} + +interface IExtendedActivity extends Activity { + [name: string]: any; +} + +interface IExtendedActivityStep extends ActivityStep { + [name: string]: any; +} + +interface IExtendedDeployment extends IntegrationDeployment { + [name: string]: any; +} + +function fetchStepName(step: Step): string { + let stepName = 'n/a'; + + if (step) { + const { name, action } = step; + stepName = name || (action && action.name ? action.name : stepName); + } + + return stepName; +} + +export class ActivityPageTable extends React.Component< + IActivityPagetableProps +> { + public render() { + return ( + + {t => ( + + {({ + activities: activitiesBase, + deployments: deploymentsBase, + // fetchActivities, + // fetchDeployments, + }) => { + const activities = activitiesBase as IExtendedActivity[]; + const integrationDeployments = (deploymentsBase || + []) as IExtendedDeployment[]; + /* + const refresh = async () => { + await fetchActivities(); + await fetchDeployments(); + }; + */ + + activities.forEach((activity: IExtendedActivity) => { + if (activity.steps && Array.isArray(activity.steps)) { + activity.steps.forEach((step: IExtendedActivityStep) => { + step.name = 'n/a'; + step.isFailed = + typeof step.failure !== 'undefined' && + step.failure.length > 0; + + const deployedIntegration = integrationDeployments.find( + deployment => deployment.version === +activity.ver + ); + if (!deployedIntegration) { + return; + } + + for (const integrationFlow of deployedIntegration!.spec! + .flows!) { + const integrationStep = integrationFlow!.steps!.find( + is => is.id === step.id + ); + if (integrationStep) { + step.name = fetchStepName(integrationStep); + break; + } + } + + const errorMessages = [ + null, + ...step.messages!, + step.failure, + ].filter(messages => !!messages); + step.output = + errorMessages.length > 0 ? errorMessages.join('\n') : ''; + }); + } + }); + + return ( + <> + {activities.map( + (activity: IExtendedActivity, activityIndex: number) => ( + ( + + ) + )} + time={new Date(activity.at).getTime().toLocaleString()} + version={activity.version} + /> + ) + )} + + ); + }} + + )} + + ); + } +} diff --git a/app/ui-react/syndesis/src/modules/integrations/pages/detail/DetailsPage.tsx b/app/ui-react/syndesis/src/modules/integrations/pages/detail/DetailsPage.tsx index 64e25fa14e6..ce787f25f18 100644 --- a/app/ui-react/syndesis/src/modules/integrations/pages/detail/DetailsPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/pages/detail/DetailsPage.tsx @@ -1,25 +1,22 @@ -import { canActivate, canDeactivate, WithIntegration } from '@syndesis/api'; +import { canActivate, WithMonitoredIntegration } from '@syndesis/api'; import { IIntegrationOverviewWithDraft } from '@syndesis/models'; import { - IMenuActions, - IntegrationDetailBreadcrumb, IntegrationDetailDescription, IntegrationDetailHistoryListView, IntegrationDetailHistoryListViewItem, IntegrationDetailHistoryListViewItemActions, - IntegrationDetailInfo, Loader, } from '@syndesis/ui'; import { WithLoader, WithRouteData } from '@syndesis/utils'; import * as React from 'react'; import { Translation } from 'react-i18next'; +import { AppContext } from '../../../../app'; import { ApiError, PageTitle } from '../../../../shared'; -import resolvers from '../../../resolvers'; import { + IntegrationDetailHeader, IntegrationDetailSteps, WithIntegrationActions, } from '../../components'; -import { IntegrationDetailNavBar } from '../../shared'; /** * @integrationId - the ID of the integration for which details are being displayed. @@ -54,126 +51,123 @@ export class DetailsPage extends React.Component { return ( {t => ( - > - {({ integrationId }) => ( - - {({ data, hasData, error }) => ( - } - errorChildren={} - > - {() => ( - - {({ - ciCdAction, - editAction, - deleteAction, - exportAction, - startAction, - stopAction, - }) => { - const breadcrumbMenuActions: IMenuActions[] = []; - if (canActivate(data)) { - breadcrumbMenuActions.push(startAction); - } - if (canDeactivate(data)) { - breadcrumbMenuActions.push(stopAction); - } - breadcrumbMenuActions.push(deleteAction); - breadcrumbMenuActions.push(ciCdAction); - return ( - <> - - - - - - - - 0} - isDraft={data.isDraft} - i18nTextDraft={t('shared:Draft')} - i18nTextHistory={t( - 'integrations:detail:History' - )} - publishAction={ - canActivate(data) - ? startAction.onClick - : undefined - } - publishHref={ - canActivate(data) - ? startAction.href - : undefined - } - publishLabel={ - canActivate(data) - ? t('shared:Publish') - : undefined - } - children={(data.deployments || []).map( - (deployment, idx) => { - return ( - - } - currentState={deployment.currentState!} - i18nTextLastPublished={t( - 'integrations:detail:lastPublished' - )} - i18nTextVersion={t('shared:Version')} - updatedAt={deployment.updatedAt} - version={deployment.version} - /> - ); - } - )} - /> - - ); - }} - + + {({ getPodLogUrl }) => ( + > + {({ integrationId }) => ( + + {({ data, hasData, error }) => ( + } + errorChildren={} + > + {() => ( + + {({ + ciCdAction, + editAction, + deleteAction, + exportAction, + startAction, + stopAction, + }) => { + return ( + <> + + + + + 0 + } + isDraft={ + (data.integration as IIntegrationOverviewWithDraft) + .isDraft + } + i18nTextDraft={t('shared:Draft')} + i18nTextHistory={t( + 'integrations:detail:History' + )} + publishAction={ + canActivate(data.integration) + ? startAction.onClick + : undefined + } + publishHref={ + canActivate(data.integration) + ? startAction.href + : undefined + } + publishLabel={ + canActivate(data.integration) + ? t('shared:Publish') + : undefined + } + children={( + data.integration.deployments || [] + ).map((deployment, idx) => { + return ( + + } + currentState={ + deployment.currentState! + } + i18nTextLastPublished={t( + 'integrations:detail:lastPublished' + )} + i18nTextVersion={t('shared:Version')} + updatedAt={deployment.updatedAt} + version={deployment.version} + /> + ); + })} + /> + + ); + }} + + )} + )} - + )} - + )} - + )} ); diff --git a/app/ui-react/syndesis/src/modules/integrations/pages/detail/MetricsPage.tsx b/app/ui-react/syndesis/src/modules/integrations/pages/detail/MetricsPage.tsx index f13c8699361..95870b516bb 100644 --- a/app/ui-react/syndesis/src/modules/integrations/pages/detail/MetricsPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/pages/detail/MetricsPage.tsx @@ -1,8 +1,15 @@ +import { WithMonitoredIntegration } from '@syndesis/api'; import { Integration } from '@syndesis/models'; -import { WithRouteData } from '@syndesis/utils'; +import { Loader } from '@syndesis/ui'; +import { WithLoader, WithRouteData } from '@syndesis/utils'; import * as React from 'react'; import { Translation } from 'react-i18next'; -import { IntegrationDetailNavBar } from '../../shared'; +import { AppContext } from '../../../../app'; +import { ApiError, PageTitle } from '../../../../shared'; +import { + IntegrationDetailHeader, + WithIntegrationActions, +} from '../../components'; /** * @integrationId - the ID of the integration for which details are being displayed. @@ -25,18 +32,67 @@ export interface IMetricsPageState { export class MetricsPage extends React.Component { public render() { return ( - > - {({ integrationId }, { integration }, { history }) => { - return ( -
- - {t => } - -

This is the Integration Detail Metrics page.

-
- ); - }} - + + {t => ( + + {({ getPodLogUrl }) => ( + > + {({ integrationId }, { integration }, { history }) => { + return ( + + {({ data, hasData, error }) => ( + } + errorChildren={} + > + {() => ( + + {({ + ciCdAction, + editAction, + deleteAction, + exportAction, + startAction, + stopAction, + }) => { + return ( + <> + + +

+ This is the Integration Detail Metrics + page. +

+ + ); + }} +
+ )} +
+ )} +
+ ); + }} + + )} +
+ )} +
); } } diff --git a/app/ui-react/syndesis/src/modules/integrations/pages/detail/index.ts b/app/ui-react/syndesis/src/modules/integrations/pages/detail/index.ts index 641a6eecc00..4eae805e1a8 100644 --- a/app/ui-react/syndesis/src/modules/integrations/pages/detail/index.ts +++ b/app/ui-react/syndesis/src/modules/integrations/pages/detail/index.ts @@ -1,3 +1,4 @@ export * from './ActivityPage'; +export * from './ActivityPageTable'; export * from './DetailsPage'; export * from './MetricsPage';