From ee9a1d97c65eac7ffc8038e92ea80bacceb80ee3 Mon Sep 17 00:00:00 2001 From: sg00dwin Date: Mon, 14 May 2018 10:23:28 -0400 Subject: [PATCH] Recreate build pipeline within tectonic console - Per task https://jira.coreos.com/browse/CONSOLE-453 - additions from @sg00dwin - Update scss to be smaller and less repetitive. - And change markup for when "no stages have started." state - Remove build details link since we're already show build details beneath pipeline --- .../public/components/build-pipeline.scss | 334 ++++++++++++++++++ frontend/public/components/build-pipeline.tsx | 171 +++++++++ frontend/public/components/build.tsx | 18 +- frontend/public/style.scss | 1 + 4 files changed, 519 insertions(+), 5 deletions(-) create mode 100644 frontend/public/components/build-pipeline.scss create mode 100644 frontend/public/components/build-pipeline.tsx diff --git a/frontend/public/components/build-pipeline.scss b/frontend/public/components/build-pipeline.scss new file mode 100644 index 00000000000..a43985ecb38 --- /dev/null +++ b/frontend/public/components/build-pipeline.scss @@ -0,0 +1,334 @@ +$pipeline-aborted-color: $color-pf-black-300; +$pipeline-border-color: $color-pf-black-300; +$pipeline-failed-color: $color-pf-red-100; +$pipeline-font-base: 12px; +$pipeline-in-progress-color: $color-pf-blue-400; +$pipeline-new-color: $color-pf-blue-100; +$pipeline-pending-color: $color-pf-black-300; +$pipeline-success-color: $color-pf-green-400; +$pipeline-require-attention-color: $color-pf-gold-400; +$pipeline-circle-animation-time: 0.35s; +$pipeline-circle-diameter: 18px; +$pipeline-line-border-width: 8px; +$pipeline-circle-border-width: ($pipeline-line-border-width / 2); +$pipeline-circle-radius: ($pipeline-circle-diameter / 2); +$pipeline-inner-circle-animation-time: 0.1s; +$pipeline-icon-animation-time: $pipeline-inner-circle-animation-time; +$pipeline-inner-circle-color: #fff; +$pipeline-line-animation-time: $pipeline-circle-animation-time; +$pipeline-line-border-width: 8px; +$pipeline-line-grow-animation-time: 0.5s; +$pipeline-line-height: ($pipeline-line-border-width / 2); +$pipeline-padding: 10px; +$pipeline-progress-line: 100%; +$pipeline-progress-rail-animation-time: 5s; +$pipeline-semi-circle-animation-time: ($pipeline-circle-animation-time / 2); + +// Animations +@keyframes build-progress-line { + to {width: $pipeline-progress-line} +} + +@keyframes build-progress-rail { + to {transform: translateX(400%)} +} + +@keyframes build-progress { + to {transform: rotate(180deg)} +} + +@keyframes pipeline-stage-fadeIcon { + to {opacity: 1} +} + +@keyframes pipeline-stage-fadeOut { + to {background-color: transparent} +} + +.build-pipeline { + border: 1px solid $pipeline-border-color; + font-size: $pipeline-font-base; + margin-bottom: 30px; + &:first-child { + border-top-width: 1px; + } + // Switch summary from top to left placement + @media (min-width: 600px) { + display: flex; + flex: 1 1 0%; + flex-direction: row; + } +} + +.build-pipeline__container { + flex: 1 1 auto; + overflow: hidden; +} +.build-pipeline__stages { + display: flex; + flex-wrap: wrap; + height: 100%; + padding: 0 ($pipeline-padding / 2); +} + +.build-pipeline__stage { + // need to use flex-basis: auto and width because of IE11 bug + flex: 0 0 auto; + min-height: 96px; + padding: ($pipeline-padding + 5) $pipeline-padding ($pipeline-padding * 3) $pipeline-padding; + position: relative; + width: 100%; + // add arrow after each stage + &:before { + bottom: 0; + color: darken($pipeline-border-color, 5%); + content: '\2193'; + font-size: 22px; + left: 0; + line-height: 1; + position: absolute; + right: 0; + text-align: center; + } + // hide arrow on last stage + &:last-child:before { + display: none; + } + @media (min-width: 480px) { + padding-right: ($pipeline-padding * 5.2); + padding-bottom: ($pipeline-padding + 5); + width: (100% / 3); + &:before { + bottom: auto; + content: '\2192'; + left: auto; + right: 10px; + top: 37%; + } + } +} + +// Set number of stages per row +$pipelineStageScreenSize: ( + 4 $screen-md-min, + 5 $screen-lg-min, + 6 ($screen-lg-min + 200), + 7 ($screen-lg-min + 400) +); + +@each $screenSize in $pipelineStageScreenSize { + @media (min-width: nth($screenSize, 2)) { + .build-pipeline__stage { + width: (100% / nth($screenSize, 1)); + } + } +} + +.build-pipeline__stage--none { + align-items: center; + display: flex; + @media (max-width: $screen-xs-max) { + justify-content: center; + } + padding: 0 $pipeline-padding; +} + +.build-pipeline__stage-name, .build-pipeline__stage-time, .build-pipeline__stage-actions { + font-size: $pipeline-font-base; + text-align: center; +} +.build-pipeline__stage-name { + @include text-overflow(); + margin-bottom: $pipeline-padding + 3px; +} +.build-pipeline__stage-time, .build-pipeline__stage-actions { + margin-top: 12px; + &--in-progress { + color: #777; + } +} + +.build-pipeline__status-icon--complete { + color: $pipeline-success-color; +} +.build-pipeline__status-icon--failed{ + color: $pipeline-failed-color; +} + +.build-pipeline__summary { + border-bottom: 1px solid $pipeline-border-color; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-around; + padding: 5px; + position: relative; + text-align: center; + @media (min-width: 600px) { + border-bottom-width: 0; + border-right: 1px solid $pipeline-border-color; + flex: 0 0 125px; + flex-direction: column; + justify-content: center; + } +} + +.build-pipeline__status-bar { + .build-pipeline__animation-line:before, + .build-pipeline__circle-clip1:before, + .build-pipeline__circle-clip2:before { + background-color: $pipeline-pending-color; + } + .build-pipeline__circle-inner-fill { + background-color: $pipeline-inner-circle-color; + opacity: 0; + } +} + +$pipelineStageStatus: ( + success Success $pipeline-success-color "\f00c" null, + failed Failed $pipeline-failed-color "\f00d" null, + not-executed NotExecuted $pipeline-pending-color "" null, + paused-pending-input null $pipeline-require-attention-color "\f04c" ($pipeline-font-base - 2), + aborted Aborted $pipeline-aborted-color "\f05e" null, + in-progress InProgress $pipeline-in-progress-color "\f021" null +); + +@each $status in $pipelineStageStatus { + .build-pipeline__status-bar--#{nth($status,1)} { + .build-pipeline__animation-line:before, + .build-pipeline__circle-clip1:before, + .build-pipeline__circle-clip2:before, + .build-pipeline__circle-inner-fill { + background-color: nth($status,3); + } + .build-pipeline__animation-circle { + @if(nth($status,2)) { + animation: pipeline-stage-fadeIn#{nth($status,2)} 0s ($pipeline-line-animation-time + $pipeline-circle-animation-time) linear forwards; + } + &:after { + content: nth($status,4); + @if(nth($status,5)) { + font-size: nth($status,5); + } + } + } + } +} + +.build-pipeline__status-bar--in-progress { + .build-pipeline__animation-line { + overflow: hidden; + &:before { + animation: build-progress-rail $pipeline-progress-rail-animation-time $pipeline-line-grow-animation-time linear infinite; + background-color: $pipeline-in-progress-color; + transform: translateX(-100%); + width: 25%; + } + } + .build-pipeline__circle-clip1:before, + .build-pipeline__circle-clip2:before, + .build-pipeline__circle-inner-fill { + background-color: $pipeline-in-progress-color; + } + .build-pipeline__animation-circle { + animation: pipeline-stage-fadeInProgress 0s ($pipeline-line-animation-time + $pipeline-circle-animation-time) linear forwards; + &:after { + content: "\f021"; + } + } +} + +.build-pipeline__status-bar { + align-items: center; + display: flex; + flex-direction: column; + margin-bottom: -($pipeline-circle-diameter / 2); +} +.build-pipeline__log-link, +.build-pipeline__timestamp { + font-size: ($pipeline-font-base - 1); +} +.build-pipeline__animation-line { + background: $pipeline-pending-color; + height: $pipeline-line-height; + position: relative; + width: $pipeline-progress-line; + &:before { + animation: build-progress-line $pipeline-line-animation-time ease-in forwards; + content: ''; + height: 100%; + position: absolute; + width: 0; + } +} +.build-pipeline__animation-circle { + background: $pipeline-pending-color; + border-radius: $pipeline-circle-radius; + height: $pipeline-circle-diameter; + margin-top: (-($pipeline-circle-diameter / 2) - ($pipeline-line-height / 2)); + position: relative; + transform: rotate(-90deg); + width: $pipeline-circle-diameter; + &:after { + animation: pipeline-stage-fadeIcon $pipeline-icon-animation-time ($pipeline-line-grow-animation-time + $pipeline-semi-circle-animation-time + ($pipeline-inner-circle-animation-time * 2)) linear forwards; + color: $pipeline-inner-circle-color; + font-family: 'FontAwesome'; + font-size: $pipeline-font-base; + left: 50%; + opacity: 0; + position: absolute; + top: 50%; + transform: translate(-50%, -50%) rotate(90deg); + } +} + +%circle-clip { + position: absolute; + z-index: -9; +} + +%circle-clip-before { + border-radius: $pipeline-circle-radius; + content: ''; + height: $pipeline-circle-diameter; + position: absolute; + transform: rotate(0); + width: $pipeline-circle-diameter; +} + +.build-pipeline__circle-clip1 { + @extend %circle-clip; + clip: rect(0, $pipeline-circle-diameter, $pipeline-circle-diameter, $pipeline-circle-radius); + &:before { + @extend %circle-clip-before; + animation: build-progress $pipeline-semi-circle-animation-time $pipeline-line-animation-time linear forwards; + clip: rect(0, $pipeline-circle-radius, $pipeline-circle-diameter, 0); + } +} +.build-pipeline__circle-clip2 { + @extend %circle-clip; + clip: rect(0, $pipeline-circle-radius, $pipeline-circle-diameter, 0); + &:before { + @extend %circle-clip-before; + animation: build-progress $pipeline-semi-circle-animation-time ($pipeline-semi-circle-animation-time + $pipeline-line-animation-time) linear forwards; + clip: rect(0, $pipeline-circle-diameter, $pipeline-circle-diameter, $pipeline-circle-radius); + } +} +.build-pipeline__circle-inner { + animation: pipeline-stage-fadeOut $pipeline-inner-circle-animation-time ($pipeline-line-animation-time + $pipeline-circle-animation-time) linear forwards; + background-color: $pipeline-inner-circle-color; + border-radius: $pipeline-circle-radius; + height: ($pipeline-circle-diameter - $pipeline-circle-border-width * 2); + left: $pipeline-circle-border-width; + position: absolute; + top: $pipeline-circle-border-width; + width: ($pipeline-circle-diameter - $pipeline-circle-border-width * 2); +} +.build-pipeline__circle-inner-fill { + border-radius: 50%; + box-sizing: border-box; + height: 100%; + opacity: 0; + width: 100%; +} diff --git a/frontend/public/components/build-pipeline.tsx b/frontend/public/components/build-pipeline.tsx new file mode 100644 index 00000000000..6f8067a651c --- /dev/null +++ b/frontend/public/components/build-pipeline.tsx @@ -0,0 +1,171 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import * as _ from 'lodash-es'; +import { resourcePath } from './utils'; +import { fromNow } from './utils/datetime'; +import { K8sResourceKind } from '../module/k8s'; + +const getBuildNumber = (resource: K8sResourceKind): number => _.get(resource, ['metadata', 'annotations', 'openshift.io/build.number']); +const getStages = (status): any[] => (status && status.stages) || []; +const getJenkinsStatus = (resource: K8sResourceKind) => { + const json = _.get(resource, ['metadata', 'annotations', 'openshift.io/jenkins-status-json']); + if (!json) { + return {}; + } + + const status = _.attempt(JSON.parse, json); + return _.isError(status) ? {} : status; +}; +const getJenkinsLogURL = (resource: K8sResourceKind): string => _.get(resource, ['metadata', 'annotations', 'openshift.io/jenkins-log-url']); +const getJenkinsBuildURL = (resource: K8sResourceKind): string => _.get(resource, ['metadata', 'annotations', 'openshift.io/jenkins-build-uri']); + +const BuildSummaryStatusIcon: React.SFC = ({ status }) => { + const statusClass = _.lowerCase(status); + const icon = ({ + new: 'fa-hourglass-o', + pending: 'fa-hourglass-half', + running: 'fa-refresh fa-spin', + complete: 'fa-check-circle', + failed: 'fa-times-circle' + })[statusClass]; + + return icon + ? + + + : + + ; +}; + +const BuildLogLink: React.SFC = ({ obj }) => { + const link = getJenkinsLogURL(obj); + return link ?
+ View Log +
: null; +}; + +const StagesNotStarted: React.SFC = () =>
+ No stages have started. +
; + +const BuildSummaryTimestamp: React.SFC = ({ timestamp }) => + {fromNow(timestamp)} +; + +const BuildPipelineSummary: React.SFC = ({ obj }) => { + const { name, namespace } = obj.metadata; + const buildNumber = getBuildNumber(obj); + const path: string = resourcePath(obj.kind, name, namespace); + return
+
+ Build {buildNumber} +
+ + +
; +}; + +const BuildAnimation: React.SFC = ({ status }) =>
+
+
+
+
+
+
+
+
+
; + +const JenkinsInputUrl: React.SFC = ({ obj, stage }) => { + const pending = stage.status === 'PAUSED_PENDING_INPUT'; + + if (!pending) { + return null; + } + + const buildUrl = getJenkinsBuildURL(obj); + return ; +}; + +const BuildStageTimestamp: React.SFC = ({ timestamp }) =>
+ {fromNow(timestamp)} +
; + +const BuildStageName: React.SFC = ({ name }) => { + return
+ {name} +
; +}; + +const BuildStage: React.SFC = ({ obj, stage }) => { + return
+
+ + + + +
+
; +}; + +export const BuildPipeline: React.SFC = ({ obj }) => { + const jenkinsStatus: any = getJenkinsStatus(obj); + const stages = getStages(jenkinsStatus); + return
+ +
+
+ {_.isEmpty(stages) + ? + : stages.map(stage => )} +
+
+
; +}; + +/* eslint-disable no-undef */ +export type BuildPipelineProps = { + obj: K8sResourceKind, +}; + +export type BuildStageProps = { + obj: K8sResourceKind, + stage: any, +}; + +export type BuildAnimationProps = { + status: string, +}; + +export type BuildPipelineSummaryProps = { + obj: K8sResourceKind, +}; + +export type BuildSummaryStatusIconProps = { + status: string, +}; + +export type BuildStageTimestampProps = { + timestamp: string, +}; + +export type BuildLogLinkProps = { + obj: K8sResourceKind, +}; + +export type BuildSummaryTimestampProps = { + timestamp: string, +}; + +export type BuildStageNameProps = { + name: string, +}; + +export type JenkinsInputUrlProps = { + obj: K8sResourceKind, + stage: any, +}; +/* eslint-disable no-undef */ diff --git a/frontend/public/components/build.tsx b/frontend/public/components/build.tsx index 148e36cf59d..3b546a52c5e 100644 --- a/frontend/public/components/build.tsx +++ b/frontend/public/components/build.tsx @@ -7,6 +7,7 @@ import { cloneBuild, formatBuildDuration } from '../module/k8s/builds'; import { ColHead, DetailsPage, List, ListHeader, ListPage } from './factory'; import { errorModal } from './modals'; import { BuildStrategy, Cog, history, navFactory, ResourceCog, ResourceLink, resourceObjPath, ResourceSummary, Timestamp } from './utils'; +import { BuildPipeline } from './build-pipeline'; import { breadcrumbsForOwnerRefs } from './utils/breadcrumbs'; import { fromNow } from './utils/datetime'; import { EnvironmentPage } from './environment'; @@ -22,7 +23,7 @@ const cloneBuildAction = (kind, build) => ({ history.push(resourceObjPath(clone, referenceFor(clone))); }).catch(err => { const error = err.message; - errorModal({error}); + errorModal({ error }); }), }); @@ -32,12 +33,18 @@ const menuActions = [ ...common, ]; -export const BuildsDetails: React.SFC = ({obj: build}) => { +export const BuildsDetails: React.SFC = ({ obj: build }) => { const triggeredBy = _.map(build.spec.triggeredBy, 'message').join(', '); const started = _.get(build, 'status.startTimestamp'); const duration = formatBuildDuration(build); + const hasPipeline = build.spec.strategy.type === 'JenkinsPipeline'; return
+ {hasPipeline &&
+
+ +
+
}
@@ -58,7 +65,8 @@ export const BuildsDetails: React.SFC = ({obj: build}) => {
-
; + + ; }; export const getStrategyType = (strategy) => { @@ -119,7 +127,7 @@ const BuildsHeader = props => Created ; -const BuildsRow: React.SFC = ({obj}) =>
+const BuildsRow: React.SFC = ({ obj }) =>
@@ -131,7 +139,7 @@ const BuildsRow: React.SFC = ({obj}) =>
- { fromNow(obj.metadata.creationTimestamp) } + {fromNow(obj.metadata.creationTimestamp)}
; diff --git a/frontend/public/style.scss b/frontend/public/style.scss index 4766d64e793..127efc60ea2 100644 --- a/frontend/public/style.scss +++ b/frontend/public/style.scss @@ -38,6 +38,7 @@ @import "components/utils/status-box"; @import "components/utils/selector"; @import "components/utils/log-window"; +@import "components/build-pipeline"; @import "components/chargeback"; @import "components/cluster-overview"; @import "components/cluster-settings/cluster-settings";