diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-04-19 16:48:33 +0300 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-04-19 16:48:33 +0300 |
commit | 6c3038e8714a2e9b959cf09cd0658b48118d2ddf (patch) | |
tree | 2850e1f499099b2e31837589e02fda6b9cd570d7 /app/assets/javascripts/pipelines/components | |
parent | dbbcb32cfef8d9304afe648bbe13e8b98fc1ee30 (diff) | |
parent | a9da37434af6d44c5f851affd4bd69b370760e8e (diff) |
Merge branch 'master' into 30985-cancel-pipelines
* master:
Remove special naming of pipelines folder
remove changelog (not needed)
Fix active user count
add spec and changelog
Add migration to remove orphaned notification settings
Improve container registry repository path specs
Fix duplicated container repository names
update textarea height and refocus when attaching files
Remove IIFEs in filtered_search_bundle.js
Remove IIFEs from diff_notes_bundle.js
Diffstat (limited to 'app/assets/javascripts/pipelines/components')
11 files changed, 687 insertions, 0 deletions
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue new file mode 100644 index 00000000000..d1c60b570de --- /dev/null +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -0,0 +1,102 @@ +<script> +/* eslint-disable no-new, no-alert */ +/* global Flash */ +import '~/flash'; +import eventHub from '../event_hub'; + +export default { + props: { + endpoint: { + type: String, + required: true, + }, + + service: { + type: Object, + required: true, + }, + + title: { + type: String, + required: true, + }, + + icon: { + type: String, + required: true, + }, + + cssClass: { + type: String, + required: true, + }, + + confirmActionMessage: { + type: String, + required: false, + }, + }, + + data() { + return { + isLoading: false, + }; + }, + + computed: { + iconClass() { + return `fa fa-${this.icon}`; + }, + + buttonClass() { + return `btn has-tooltip ${this.cssClass}`; + }, + }, + + methods: { + onClick() { + if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { + this.makeRequest(); + } else if (!this.confirmActionMessage) { + this.makeRequest(); + } + }, + + makeRequest() { + this.isLoading = true; + + $(this.$el).tooltip('destroy'); + + this.service.postAction(this.endpoint) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshPipelines'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, + }, +}; +</script> + +<template> + <button + type="button" + @click="onClick" + :class="buttonClass" + :title="title" + :aria-label="title" + data-container="body" + data-placement="top" + :disabled="isLoading"> + <i + :class="iconClass" + aria-hidden="true" /> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" + v-if="isLoading" /> + </button> +</template> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue new file mode 100644 index 00000000000..ba158bc4a1e --- /dev/null +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -0,0 +1,34 @@ +<script> +import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg'; + +export default { + props: { + helpPagePath: { + type: String, + required: true, + }, + }, + data: () => ({ pipelinesEmptyStateSVG }), +}; +</script> + +<template> + <div class="row empty-state"> + <div class="col-xs-12"> + <div class="svg-content" v-html="pipelinesEmptyStateSVG" /> + </div> + + <div class="col-xs-12 text-center"> + <div class="text-content"> + <h4>Build with confidence</h4> + <p> + Continous Integration can help catch bugs by running your tests automatically, + while Continuous Deployment can help you deliver code to your product environment. + </p> + <a :href="helpPagePath" class="btn btn-info"> + Get started with Pipelines + </a> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue new file mode 100644 index 00000000000..90cee68163e --- /dev/null +++ b/app/assets/javascripts/pipelines/components/error_state.vue @@ -0,0 +1,21 @@ +<script> +import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg'; + +export default { + data: () => ({ pipelinesErrorStateSVG }), +}; +</script> + +<template> + <div class="row empty-state js-pipelines-error-state"> + <div class="col-xs-12"> + <div class="svg-content" v-html="pipelinesErrorStateSVG" /> + </div> + + <div class="col-xs-12 text-center"> + <div class="text-content"> + <h4>The API failed to fetch the pipelines.</h4> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/nav_controls.js b/app/assets/javascripts/pipelines/components/nav_controls.js new file mode 100644 index 00000000000..6aa10531034 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/nav_controls.js @@ -0,0 +1,52 @@ +export default { + props: { + newPipelinePath: { + type: String, + required: true, + }, + + hasCiEnabled: { + type: Boolean, + required: true, + }, + + helpPagePath: { + type: String, + required: true, + }, + + ciLintPath: { + type: String, + required: true, + }, + + canCreatePipeline: { + type: Boolean, + required: true, + }, + }, + + template: ` + <div class="nav-controls"> + <a + v-if="canCreatePipeline" + :href="newPipelinePath" + class="btn btn-create"> + Run Pipeline + </a> + + <a + v-if="!hasCiEnabled" + :href="helpPagePath" + class="btn btn-info"> + Get started with Pipelines + </a> + + <a + :href="ciLintPath" + class="btn btn-default"> + CI Lint + </a> + </div> + `, +}; diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.js b/app/assets/javascripts/pipelines/components/navigation_tabs.js new file mode 100644 index 00000000000..1626ae17a30 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/navigation_tabs.js @@ -0,0 +1,72 @@ +export default { + props: { + scope: { + type: String, + required: true, + }, + + count: { + type: Object, + required: true, + }, + + paths: { + type: Object, + required: true, + }, + }, + + mounted() { + $(document).trigger('init.scrolling-tabs'); + }, + + template: ` + <ul class="nav-links scrolling-tabs"> + <li + class="js-pipelines-tab-all" + :class="{ 'active': scope === 'all'}"> + <a :href="paths.allPath"> + All + <span class="badge js-totalbuilds-count"> + {{count.all}} + </span> + </a> + </li> + <li class="js-pipelines-tab-pending" + :class="{ 'active': scope === 'pending'}"> + <a :href="paths.pendingPath"> + Pending + <span class="badge"> + {{count.pending}} + </span> + </a> + </li> + <li class="js-pipelines-tab-running" + :class="{ 'active': scope === 'running'}"> + <a :href="paths.runningPath"> + Running + <span class="badge"> + {{count.running}} + </span> + </a> + </li> + <li class="js-pipelines-tab-finished" + :class="{ 'active': scope === 'finished'}"> + <a :href="paths.finishedPath"> + Finished + <span class="badge"> + {{count.finished}} + </span> + </a> + </li> + <li class="js-pipelines-tab-branches" + :class="{ 'active': scope === 'branches'}"> + <a :href="paths.branchesPath">Branches</a> + </li> + <li class="js-pipelines-tab-tags" + :class="{ 'active': scope === 'tags'}"> + <a :href="paths.tagsPath">Tags</a> + </li> + </ul> + `, +}; diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.js b/app/assets/javascripts/pipelines/components/pipeline_url.js new file mode 100644 index 00000000000..4e183d5c8ec --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_url.js @@ -0,0 +1,56 @@ +export default { + props: [ + 'pipeline', + ], + computed: { + user() { + return !!this.pipeline.user; + }, + }, + template: ` + <td> + <a + :href="pipeline.path" + class="js-pipeline-url-link"> + <span class="pipeline-id">#{{pipeline.id}}</span> + </a> + <span>by</span> + <a + class="js-pipeline-url-user" + v-if="user" + :href="pipeline.user.web_url"> + <img + v-if="user" + class="avatar has-tooltip s20 " + :title="pipeline.user.name" + data-container="body" + :src="pipeline.user.avatar_url" + > + </a> + <span + v-if="!user" + class="js-pipeline-url-api api monospace"> + API + </span> + <span + v-if="pipeline.flags.latest" + class="js-pipeline-url-lastest label label-success has-tooltip" + title="Latest pipeline for this branch" + data-original-title="Latest pipeline for this branch"> + latest + </span> + <span + v-if="pipeline.flags.yaml_errors" + class="js-pipeline-url-yaml label label-danger has-tooltip" + :title="pipeline.yaml_errors" + :data-original-title="pipeline.yaml_errors"> + yaml invalid + </span> + <span + v-if="pipeline.flags.stuck" + class="js-pipeline-url-stuck label label-warning"> + stuck + </span> + </td> + `, +}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.js b/app/assets/javascripts/pipelines/components/pipelines_actions.js new file mode 100644 index 00000000000..535064f610e --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.js @@ -0,0 +1,88 @@ +/* eslint-disable no-new */ +/* global Flash */ +import '~/flash'; +import playIconSvg from 'icons/_icon_play.svg'; +import eventHub from '../event_hub'; + +export default { + props: { + actions: { + type: Array, + required: true, + }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + playIconSvg, + isLoading: false, + }; + }, + + methods: { + onClickAction(endpoint) { + this.isLoading = true; + + $(this.$el.querySelector('.has-tooltip')).tooltip('destroy'); + + this.service.postAction(endpoint) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshPipelines'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, + + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } + + return !action.playable; + }, + }, + + template: ` + <div class="btn-group" v-if="actions"> + <button + type="button" + class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" + title="Manual job" + data-toggle="dropdown" + data-placement="top" + aria-label="Manual job" + :disabled="isLoading"> + ${playIconSvg} + <i + class="fa fa-caret-down" + aria-hidden="true" /> + <i + v-if="isLoading" + class="fa fa-spinner fa-spin" + aria-hidden="true" /> + </button> + + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="action in actions"> + <button + type="button" + class="js-pipeline-action-link no-btn btn" + @click="onClickAction(action.path)" + :class="{ 'disabled': isActionDisabled(action) }" + :disabled="isActionDisabled(action)"> + ${playIconSvg} + <span>{{action.name}}</span> + </button> + </li> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.js b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js new file mode 100644 index 00000000000..f18e2dfadaf --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js @@ -0,0 +1,33 @@ +export default { + props: { + artifacts: { + type: Array, + required: true, + }, + }, + + template: ` + <div class="btn-group" role="group"> + <button + class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" + title="Artifacts" + data-placement="top" + data-toggle="dropdown" + aria-label="Artifacts"> + <i class="fa fa-download" aria-hidden="true"></i> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="artifact in artifacts"> + <a + rel="nofollow" + download + :href="artifact.path"> + <i class="fa fa-download" aria-hidden="true"></i> + <span>Download {{artifact.name}} artifacts</span> + </a> + </li> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/pipelines/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js new file mode 100644 index 00000000000..b8cc3630611 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/stage.js @@ -0,0 +1,98 @@ +/* global Flash */ +import StatusIconEntityMap from '../../ci_status_icons'; + +export default { + data() { + return { + builds: '', + spinner: '<span class="fa fa-spinner fa-spin"></span>', + }; + }, + + props: { + stage: { + type: Object, + required: true, + }, + }, + + updated() { + if (this.builds) { + this.stopDropdownClickPropagation(); + } + }, + + methods: { + fetchBuilds(e) { + const ariaExpanded = e.currentTarget.attributes['aria-expanded']; + + if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null; + + return this.$http.get(this.stage.dropdown_path) + .then((response) => { + this.builds = JSON.parse(response.body).html; + }, () => { + const flash = new Flash('Something went wrong on our end.'); + return flash; + }); + }, + + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { + e.stopPropagation(); + }); + }, + }, + computed: { + buildsOrSpinner() { + return this.builds ? this.builds : this.spinner; + }, + dropdownClass() { + if (this.builds) return 'js-builds-dropdown-container'; + return 'js-builds-dropdown-loading builds-dropdown-loading'; + }, + buildStatus() { + return `Build: ${this.stage.status.label}`; + }, + tooltip() { + return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; + }, + triggerButtonClass() { + return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; + }, + svgHTML() { + return StatusIconEntityMap[this.stage.status.icon]; + }, + }, + template: ` + <div> + <button + @click="fetchBuilds($event)" + :class="triggerButtonClass" + :title="stage.title" + data-placement="top" + data-toggle="dropdown" + type="button" + :aria-label="stage.title"> + <span v-html="svgHTML" aria-hidden="true"></span> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> + <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> + <div class="arrow-up" aria-hidden="true"></div> + <div + :class="dropdownClass" + class="js-builds-dropdown-list scrollable-menu" + v-html="buildsOrSpinner"> + </div> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/pipelines/components/status.js b/app/assets/javascripts/pipelines/components/status.js new file mode 100644 index 00000000000..21a281af438 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/status.js @@ -0,0 +1,60 @@ +import canceledSvg from 'icons/_icon_status_canceled.svg'; +import createdSvg from 'icons/_icon_status_created.svg'; +import failedSvg from 'icons/_icon_status_failed.svg'; +import manualSvg from 'icons/_icon_status_manual.svg'; +import pendingSvg from 'icons/_icon_status_pending.svg'; +import runningSvg from 'icons/_icon_status_running.svg'; +import skippedSvg from 'icons/_icon_status_skipped.svg'; +import successSvg from 'icons/_icon_status_success.svg'; +import warningSvg from 'icons/_icon_status_warning.svg'; + +export default { + props: { + pipeline: { + type: Object, + required: true, + }, + }, + + data() { + const svgsDictionary = { + icon_status_canceled: canceledSvg, + icon_status_created: createdSvg, + icon_status_failed: failedSvg, + icon_status_manual: manualSvg, + icon_status_pending: pendingSvg, + icon_status_running: runningSvg, + icon_status_skipped: skippedSvg, + icon_status_success: successSvg, + icon_status_warning: warningSvg, + }; + + return { + svg: svgsDictionary[this.pipeline.details.status.icon], + }; + }, + + computed: { + cssClasses() { + return `ci-status ci-${this.pipeline.details.status.group}`; + }, + + detailsPath() { + const { status } = this.pipeline.details; + return status.has_details ? status.details_path : false; + }, + + content() { + return `${this.svg} ${this.pipeline.details.status.text}`; + }, + }, + template: ` + <td class="commit-link"> + <a + :class="cssClasses" + :href="detailsPath" + v-html="content"> + </a> + </td> + `, +}; diff --git a/app/assets/javascripts/pipelines/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js new file mode 100644 index 00000000000..498d0715f54 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/time_ago.js @@ -0,0 +1,71 @@ +import iconTimerSvg from 'icons/_icon_timer.svg'; +import '../../lib/utils/datetime_utility'; + +export default { + data() { + return { + currentTime: new Date(), + iconTimerSvg, + }; + }, + props: ['pipeline'], + computed: { + timeAgo() { + return gl.utils.getTimeago(); + }, + localTimeFinished() { + return gl.utils.formatDate(this.pipeline.details.finished_at); + }, + timeStopped() { + const changeTime = this.currentTime; + const options = { + weekday: 'long', + year: 'numeric', + month: 'short', + day: 'numeric', + }; + options.timeZoneName = 'short'; + const finished = this.pipeline.details.finished_at; + if (!finished && changeTime) return false; + return ({ words: this.timeAgo.format(finished) }); + }, + duration() { + const { duration } = this.pipeline.details; + const date = new Date(duration * 1000); + + let hh = date.getUTCHours(); + let mm = date.getUTCMinutes(); + let ss = date.getSeconds(); + + if (hh < 10) hh = `0${hh}`; + if (mm < 10) mm = `0${mm}`; + if (ss < 10) ss = `0${ss}`; + + if (duration !== null) return `${hh}:${mm}:${ss}`; + return false; + }, + }, + methods: { + changeTime() { + this.currentTime = new Date(); + }, + }, + template: ` + <td class="pipelines-time-ago"> + <p class="duration" v-if='duration'> + <span v-html="iconTimerSvg"></span> + {{duration}} + </p> + <p class="finished-at" v-if='timeStopped'> + <i class="fa fa-calendar"></i> + <time + data-toggle="tooltip" + data-placement="top" + data-container="body" + :data-original-title='localTimeFinished'> + {{timeStopped.words}} + </time> + </p> + </td> + `, +}; |