diff --git a/app/scripts/modules/core/src/pipeline/config/graph/PipelineGraph.tsx b/app/scripts/modules/core/src/pipeline/config/graph/PipelineGraph.tsx index f17974fb713..a661044964d 100644 --- a/app/scripts/modules/core/src/pipeline/config/graph/PipelineGraph.tsx +++ b/app/scripts/modules/core/src/pipeline/config/graph/PipelineGraph.tsx @@ -378,16 +378,16 @@ export class PipelineGraph extends React.Component; @@ -68,14 +66,13 @@ export class PipelineGraphService { const node: IPipelineGraphNode = { childLinks: [], children: [], - executionId: execution.id, executionStage: true, extraLabelLines: stage.extraLabelLines ? stage.extraLabelLines(stage) : 0, graphRowOverride: stage.graphRowOverride || 0, hasNotStarted: stage.hasNotStarted, id: stage.refId, index: idx, - isActive: viewState.activeStageId === stage.index && viewState.executionId === execution.id, + isActive: viewState.activeStageId === stage.index, isHighlighted: false, labelComponent: stage.labelComponent, masterStage: stage.masterStage, diff --git a/app/scripts/modules/core/src/pipeline/executions/execution/Execution.tsx b/app/scripts/modules/core/src/pipeline/executions/execution/Execution.tsx index c49237dc67c..2e8b90524ee 100644 --- a/app/scripts/modules/core/src/pipeline/executions/execution/Execution.tsx +++ b/app/scripts/modules/core/src/pipeline/executions/execution/Execution.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import * as ReactGA from 'react-ga'; -import { clone, isEqual } from 'lodash'; +import { isEqual } from 'lodash'; import { $location } from 'ngimport'; import { Subscription } from 'rxjs'; import * as classNames from 'classnames'; import { Application } from 'core/application/application.model'; -import { CopyToClipboard } from 'core/utils'; import { StageExecutionDetails } from 'core/pipeline/details/StageExecutionDetails'; import { ExecutionStatus } from 'core/pipeline/status/ExecutionStatus'; import { ParametersAndArtifacts } from 'core/pipeline/status/ParametersAndArtifacts'; @@ -25,6 +24,7 @@ import { ExecutionMarker } from './ExecutionMarker'; import { PipelineGraph } from 'core/pipeline/config/graph/PipelineGraph'; import { Tooltip } from 'core/presentation/Tooltip'; import { CancelModal } from 'core/cancelModal/CancelModal'; +import { ExecutionPermalink } from './ExecutionPermalink'; import './execution.less'; @@ -53,7 +53,7 @@ export interface IExecutionState { runningTimeInMs: number; } -export class Execution extends React.Component { +export class Execution extends React.PureComponent { public static defaultProps: Partial = { dataSourceKey: 'executions', cancelHelpText: 'Cancel execution', @@ -89,20 +89,23 @@ export class Execution extends React.Component }; } - private updateViewStateDetails(): void { - const { $stateParams } = ReactInjector; + private updateViewStateDetails(toParams: any, fromParams: any): void { + const executionId = this.props.execution.id; const { viewState } = this.state; - const newViewState = clone(viewState); - newViewState.activeStageId = Number($stateParams.stage); - newViewState.activeSubStageId = Number($stateParams.subStage); - newViewState.executionId = $stateParams.executionId; - const shouldScroll = - newViewState.executionId === this.props.execution.id && newViewState.executionId !== viewState.executionId; - if (!isEqual(viewState, newViewState)) { + const shouldShowDetails = toParams.executionId === executionId; + const shouldScroll = toParams.executionId === executionId && fromParams.executionId !== executionId; + const newViewState = { ...viewState }; + newViewState.activeStageId = Number(toParams.stage); + newViewState.activeSubStageId = Number(toParams.subStage); + if (this.state.showingDetails !== shouldShowDetails) { this.setState({ - viewState: newViewState, showingDetails: this.invalidateShowingDetails(this.props, shouldScroll), + viewState: newViewState, }); + } else { + if (this.state.showingDetails && !isEqual(viewState, newViewState)) { + this.setState({ viewState: newViewState }); + } } } @@ -199,8 +202,10 @@ export class Execution extends React.Component this.runningTime = new OrchestratedItemRunningTime(execution, (time: number) => this.setState({ runningTimeInMs: time }), ); - this.stateChangeSuccessSubscription = ReactInjector.stateEvents.stateChangeSuccess.subscribe(() => - this.updateViewStateDetails(), + this.stateChangeSuccessSubscription = ReactInjector.stateEvents.stateChangeSuccess.subscribe( + ({ toParams, fromParams }) => { + this.updateViewStateDetails(toParams, fromParams); + }, ); } @@ -257,10 +262,6 @@ export class Execution extends React.Component ReactGA.event({ category: 'Pipeline', action: 'Execution source clicked' }); }; - private handlePermalinkClick = (): void => { - ReactGA.event({ category: 'Pipeline', action: 'Permalink clicked' }); - }; - private handleToggleDetails = (): void => { ReactGA.event({ category: 'Pipeline', action: 'Execution details toggled (Details link)' }); this.toggleDetails(); @@ -440,10 +441,7 @@ export class Execution extends React.Component Source {' | '} - - Permalink - - + diff --git a/app/scripts/modules/core/src/pipeline/executions/execution/ExecutionPermalink.tsx b/app/scripts/modules/core/src/pipeline/executions/execution/ExecutionPermalink.tsx new file mode 100644 index 00000000000..953665b4fd1 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/executions/execution/ExecutionPermalink.tsx @@ -0,0 +1,39 @@ +import { CopyToClipboard } from 'core/utils'; +import { ReactInjector } from 'core/reactShims'; +import * as React from 'react'; +import * as ReactGA from 'react-ga'; + +export interface IExecutionPermalinkProps { + standalone: boolean; +} + +export const ExecutionPermalink = ({ standalone }: IExecutionPermalinkProps) => { + const [url, setUrl] = React.useState(location.href); + + React.useEffect(() => { + const subscription = ReactInjector.stateEvents.locationChangeSuccess.subscribe(() => { + const newUrl = location.href; + if (url !== newUrl) { + if (!standalone) { + setUrl(newUrl); + } else { + setUrl(newUrl.replace('/executions', '/executions/details')); + } + } + }); + return () => subscription.unsubscribe(); + }, []); + + const handlePermalinkClick = (): void => { + ReactGA.event({ category: 'Pipeline', action: 'Permalink clicked' }); + }; + + return ( + <> + + Permalink + + + + ); +}; diff --git a/app/scripts/modules/core/src/reactShims/state.events.ts b/app/scripts/modules/core/src/reactShims/state.events.ts index 44bfd02c64e..dcff3f0e1f9 100644 --- a/app/scripts/modules/core/src/reactShims/state.events.ts +++ b/app/scripts/modules/core/src/reactShims/state.events.ts @@ -11,6 +11,7 @@ export interface IStateChange { export class StateEvents { public stateChangeSuccess: Subject = new Subject(); + public locationChangeSuccess: Subject = new Subject(); public static $inject = ['$rootScope']; constructor(private $rootScope: IRootScopeService) { @@ -23,7 +24,10 @@ export class StateEvents { ) => { this.stateChangeSuccess.next({ to, toParams, from, fromParams }); }; + const onLocationChangeSuccess = (_event: IAngularEvent, newUrl: string) => this.locationChangeSuccess.next(newUrl); + this.$rootScope.$on('$stateChangeSuccess', onChangeSuccess); + this.$rootScope.$on('$locationChangeSuccess', onLocationChangeSuccess); } }