summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipelines
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipelines')
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue104
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue34
-rw-r--r--app/assets/javascripts/pipelines/components/error_state.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue64
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue56
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue86
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue113
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue124
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue83
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.js52
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.js72
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.js56
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.js91
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.js33
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue170
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.js98
-rw-r--r--app/assets/javascripts/pipelines/event_hub.js3
-rw-r--r--app/assets/javascripts/pipelines/graph_bundle.js10
-rw-r--r--app/assets/javascripts/pipelines/index.js22
-rw-r--r--app/assets/javascripts/pipelines/pipelines.js295
-rw-r--r--app/assets/javascripts/pipelines/services/pipeline_service.js14
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js45
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js11
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js30
25 files changed, 1724 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..37a6f02d8fd
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/async_button.vue
@@ -0,0 +1,104 @@
+<script>
+/* eslint-disable no-new, no-alert */
+/* global Flash */
+import '~/flash';
+import eventHub from '../event_hub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+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,
+ },
+ },
+
+ components: {
+ loadingIcon,
+ },
+
+ 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" />
+ <loading-icon 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..3db64339a62
--- /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 js-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/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
new file mode 100644
index 00000000000..1f9e3d39779
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -0,0 +1,64 @@
+<script>
+ import getActionIcon from '../../../vue_shared/ci_action_icons';
+ import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+
+ /**
+ * Renders either a cancel, retry or play icon pointing to the given path.
+ * TODO: Remove UJS from here and use an async request instead.
+ */
+ export default {
+ props: {
+ tooltipText: {
+ type: String,
+ required: true,
+ },
+
+ link: {
+ type: String,
+ required: true,
+ },
+
+ actionMethod: {
+ type: String,
+ required: true,
+ },
+
+ actionIcon: {
+ type: String,
+ required: true,
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ ],
+
+ computed: {
+ actionIconSvg() {
+ return getActionIcon(this.actionIcon);
+ },
+
+ cssClass() {
+ return `js-${gl.text.dasherize(this.actionIcon)}`;
+ },
+ },
+ };
+</script>
+<template>
+ <a
+ :data-method="actionMethod"
+ :title="tooltipText"
+ :href="link"
+ ref="tooltip"
+ class="ci-action-icon-container"
+ data-toggle="tooltip"
+ data-container="body">
+
+ <i
+ class="ci-action-icon-wrapper"
+ :class="cssClass"
+ v-html="actionIconSvg"
+ aria-hidden="true"
+ />
+ </a>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
new file mode 100644
index 00000000000..19cafff4e1c
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
@@ -0,0 +1,56 @@
+<script>
+ import getActionIcon from '../../../vue_shared/ci_action_icons';
+ import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+
+ /**
+ * Renders either a cancel, retry or play icon pointing to the given path.
+ * TODO: Remove UJS from here and use an async request instead.
+ */
+ export default {
+ props: {
+ tooltipText: {
+ type: String,
+ required: true,
+ },
+
+ link: {
+ type: String,
+ required: true,
+ },
+
+ actionMethod: {
+ type: String,
+ required: true,
+ },
+
+ actionIcon: {
+ type: String,
+ required: true,
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ ],
+
+ computed: {
+ actionIconSvg() {
+ return getActionIcon(this.actionIcon);
+ },
+ },
+ };
+</script>
+<template>
+ <a
+ :data-method="actionMethod"
+ :title="tooltipText"
+ :href="link"
+ ref="tooltip"
+ rel="nofollow"
+ class="ci-action-icon-wrapper js-ci-status-icon"
+ data-toggle="tooltip"
+ data-container="body"
+ v-html="actionIconSvg"
+ aria-label="Job's action">
+ </a>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
new file mode 100644
index 00000000000..d597af8dfb5
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -0,0 +1,86 @@
+<script>
+ import jobNameComponent from './job_name_component.vue';
+ import jobComponent from './job_component.vue';
+ import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+
+ /**
+ * Renders the dropdown for the pipeline graph.
+ *
+ * The following object should be provided as `job`:
+ *
+ * {
+ * "id": 4256,
+ * "name": "test",
+ * "status": {
+ * "icon": "icon_status_success",
+ * "text": "passed",
+ * "label": "passed",
+ * "group": "success",
+ * "details_path": "/root/ci-mock/builds/4256",
+ * "action": {
+ * "icon": "icon_action_retry",
+ * "title": "Retry",
+ * "path": "/root/ci-mock/builds/4256/retry",
+ * "method": "post"
+ * }
+ * }
+ * }
+ */
+ export default {
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ ],
+
+ components: {
+ jobComponent,
+ jobNameComponent,
+ },
+
+ computed: {
+ tooltipText() {
+ return `${this.job.name} - ${this.job.status.label}`;
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <button
+ type="button"
+ data-toggle="dropdown"
+ data-container="body"
+ class="dropdown-menu-toggle build-content"
+ :title="tooltipText"
+ ref="tooltip">
+
+ <job-name-component
+ :name="job.name"
+ :status="job.status" />
+
+ <span class="dropdown-counter-badge">
+ {{job.size}}
+ </span>
+ </button>
+
+ <ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown">
+ <li class="scrollable-menu">
+ <ul>
+ <li v-for="item in job.jobs">
+ <job-component
+ :job="item"
+ :is-dropdown="true"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
new file mode 100644
index 00000000000..14c98847d93
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -0,0 +1,113 @@
+<script>
+ /* global Flash */
+ import Visibility from 'visibilityjs';
+ import Poll from '../../../lib/utils/poll';
+ import PipelineService from '../../services/pipeline_service';
+ import PipelineStore from '../../stores/pipeline_store';
+ import stageColumnComponent from './stage_column_component.vue';
+ import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+ import '../../../flash';
+
+ export default {
+ components: {
+ stageColumnComponent,
+ loadingIcon,
+ },
+
+ data() {
+ const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset;
+ const store = new PipelineStore();
+
+ return {
+ isLoading: false,
+ endpoint: DOMdata.endpoint,
+ store,
+ state: store.state,
+ };
+ },
+
+ created() {
+ this.service = new PipelineService(this.endpoint);
+
+ const poll = new Poll({
+ resource: this.service,
+ method: 'getPipeline',
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
+ },
+
+ methods: {
+ successCallback(response) {
+ const data = response.json();
+
+ this.isLoading = false;
+ this.store.storeGraph(data.details.stages);
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ return new Flash('An error occurred while fetching the pipeline.');
+ },
+
+ capitalizeStageName(name) {
+ return name.charAt(0).toUpperCase() + name.slice(1);
+ },
+
+ isFirstColumn(index) {
+ return index === 0;
+ },
+
+ stageConnectorClass(index, stage) {
+ let className;
+
+ // If it's the first stage column and only has one job
+ if (index === 0 && stage.groups.length === 1) {
+ className = 'no-margin';
+ } else if (index > 0) {
+ // If it is not the first column
+ className = 'left-margin';
+ }
+
+ return className;
+ },
+ },
+ };
+</script>
+<template>
+ <div class="build-content middle-block js-pipeline-graph">
+ <div class="pipeline-visualization pipeline-graph">
+ <div class="text-center">
+ <loading-icon
+ v-if="isLoading"
+ size="3"
+ />
+ </div>
+
+ <ul
+ v-if="!isLoading"
+ class="stage-column-list">
+ <stage-column-component
+ v-for="(stage, index) in state.graph"
+ :title="capitalizeStageName(stage.name)"
+ :jobs="stage.groups"
+ :key="stage.name"
+ :stage-connector-class="stageConnectorClass(index, stage)"
+ :is-first-column="isFirstColumn(index)"/>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
new file mode 100644
index 00000000000..b39c936101e
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -0,0 +1,124 @@
+<script>
+ import actionComponent from './action_component.vue';
+ import dropdownActionComponent from './dropdown_action_component.vue';
+ import jobNameComponent from './job_name_component.vue';
+ import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+
+ /**
+ * Renders the badge for the pipeline graph and the job's dropdown.
+ *
+ * The following object should be provided as `job`:
+ *
+ * {
+ * "id": 4256,
+ * "name": "test",
+ * "status": {
+ * "icon": "icon_status_success",
+ * "text": "passed",
+ * "label": "passed",
+ * "group": "success",
+ * "details_path": "/root/ci-mock/builds/4256",
+ * "action": {
+ * "icon": "icon_action_retry",
+ * "title": "Retry",
+ * "path": "/root/ci-mock/builds/4256/retry",
+ * "method": "post"
+ * }
+ * }
+ * }
+ */
+
+ export default {
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+
+ cssClassJobName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ isDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ components: {
+ actionComponent,
+ dropdownActionComponent,
+ jobNameComponent,
+ },
+
+ mixins: [
+ tooltipMixin,
+ ],
+
+ computed: {
+ tooltipText() {
+ return `${this.job.name} - ${this.job.status.label}`;
+ },
+
+ /**
+ * Verifies if the provided job has an action path
+ *
+ * @return {Boolean}
+ */
+ hasAction() {
+ return this.job.status && this.job.status.action && this.job.status.action.path;
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <a
+ v-if="job.status.details_path"
+ :href="job.status.details_path"
+ :title="tooltipText"
+ :class="cssClassJobName"
+ ref="tooltip"
+ data-toggle="tooltip"
+ data-container="body">
+
+ <job-name-component
+ :name="job.name"
+ :status="job.status"
+ />
+ </a>
+
+ <div
+ v-else
+ :title="tooltipText"
+ :class="cssClassJobName"
+ ref="tooltip"
+ data-toggle="tooltip"
+ data-container="body">
+
+ <job-name-component
+ :name="job.name"
+ :status="job.status"
+ />
+ </div>
+
+ <action-component
+ v-if="hasAction && !isDropdown"
+ :tooltip-text="job.status.action.title"
+ :link="job.status.action.path"
+ :action-icon="job.status.action.icon"
+ :action-method="job.status.action.method"
+ />
+
+ <dropdown-action-component
+ v-if="hasAction && isDropdown"
+ :tooltip-text="job.status.action.title"
+ :link="job.status.action.path"
+ :action-icon="job.status.action.icon"
+ :action-method="job.status.action.method"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
new file mode 100644
index 00000000000..d8856e10668
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -0,0 +1,37 @@
+<script>
+ import ciIcon from '../../../vue_shared/components/ci_icon.vue';
+
+ /**
+ * Component that renders both the CI icon status and the job name.
+ * Used in
+ * - Badge component
+ * - Dropdown badge components
+ */
+ export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+
+ status: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ ciIcon,
+ },
+ };
+</script>
+<template>
+ <span>
+ <ci-icon
+ :status="status" />
+
+ <span class="ci-status-text">
+ {{name}}
+ </span>
+ </span>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
new file mode 100644
index 00000000000..9b1bbb0906f
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -0,0 +1,83 @@
+<script>
+import jobComponent from './job_component.vue';
+import dropdownJobComponent from './dropdown_job_component.vue';
+
+export default {
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+
+ jobs: {
+ type: Array,
+ required: true,
+ },
+
+ isFirstColumn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ stageConnectorClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ components: {
+ jobComponent,
+ dropdownJobComponent,
+ },
+
+ methods: {
+ firstJob(list) {
+ return list[0];
+ },
+
+ jobId(job) {
+ return `ci-badge-${job.name}`;
+ },
+
+ buildConnnectorClass(index) {
+ return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
+ },
+ },
+};
+</script>
+<template>
+ <li
+ class="stage-column"
+ :class="stageConnectorClass">
+ <div class="stage-name">
+ {{title}}
+ </div>
+ <div class="builds-container">
+ <ul>
+ <li
+ v-for="(job, index) in jobs"
+ :key="job.id"
+ class="build"
+ :class="buildConnnectorClass(index)"
+ :id="jobId(job)">
+
+ <div class="curve"></div>
+
+ <job-component
+ v-if="job.size === 1"
+ :job="job"
+ css-class-job-name="build-content"
+ />
+
+ <dropdown-job-component
+ v-if="job.size > 1"
+ :job="job"
+ />
+
+ </li>
+ </ul>
+ </div>
+ </li>
+</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..7cd2e0f9366
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.js
@@ -0,0 +1,56 @@
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+
+export default {
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ components: {
+ userAvatarLink,
+ },
+ template: `
+ <td>
+ <a
+ :href="pipeline.path"
+ class="js-pipeline-url-link">
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <user-avatar-link
+ v-if="user"
+ class="js-pipeline-url-user"
+ :link-href="pipeline.user.web_url"
+ :img-src="pipeline.user.avatar_url"
+ :tooltip-text="pipeline.user.name"
+ />
+ <span
+ v-if="!user"
+ class="js-pipeline-url-api api">
+ 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..b9e066c5db1
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.js
@@ -0,0 +1,91 @@
+/* eslint-disable no-new */
+/* global Flash */
+import '~/flash';
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+import loadingIconComponent from '../../vue_shared/components/loading_icon.vue';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ loadingIconComponent,
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ $(this.$refs.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"
+ ref="tooltip"
+ :disabled="isLoading">
+ ${playIconSvg}
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true" />
+ <loading-icon v-if="isLoading" />
+ </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.vue b/app/assets/javascripts/pipelines/components/stage.vue
new file mode 100644
index 00000000000..7fc19fce1ff
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -0,0 +1,170 @@
+<script>
+
+/**
+ * Renders each stage of the pipeline mini graph.
+ *
+ * Given the provided endpoint will make a request to
+ * fetch the dropdown data when the stage is clicked.
+ *
+ * Request is made inside this component to make it reusable between:
+ * 1. Pipelines main table
+ * 2. Pipelines table in commit and Merge request views
+ * 3. Merge request widget
+ * 4. Commit widget
+ */
+
+/* global Flash */
+import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+export default {
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+
+ updateDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ dropdownContent: '',
+ endpoint: this.stage.dropdown_path,
+ };
+ },
+
+ components: {
+ loadingIcon,
+ },
+
+ updated() {
+ if (this.dropdownContent.length > 0) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ watch: {
+ updateDropdown() {
+ if (this.updateDropdown &&
+ this.isDropdownOpen() &&
+ !this.isLoading) {
+ this.fetchJobs();
+ }
+ },
+ },
+
+ methods: {
+ onClickStage() {
+ if (!this.isDropdownOpen()) {
+ this.isLoading = true;
+ this.fetchJobs();
+ }
+ },
+
+ fetchJobs() {
+ this.$http.get(this.endpoint)
+ .then((response) => {
+ this.dropdownContent = response.json().html;
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.closeDropdown();
+ this.isLoading = false;
+
+ 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();
+ });
+ },
+
+ closeDropdown() {
+ if (this.isDropdownOpen()) {
+ $(this.$refs.dropdown).dropdown('toggle');
+ }
+ },
+
+ isDropdownOpen() {
+ return this.$el.classList.contains('open');
+ },
+ },
+
+ computed: {
+ dropdownClass() {
+ return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
+ },
+
+ triggerButtonClass() {
+ return `ci-status-icon-${this.stage.status.group}`;
+ },
+
+ svgIcon() {
+ return borderlessStatusIconEntityMap[this.stage.status.icon];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown">
+ <button
+ :class="triggerButtonClass"
+ @click="onClickStage"
+ class="mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button"
+ :title="stage.title"
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ id="stageDropdown"
+ aria-haspopup="true"
+ aria-expanded="false">
+
+ <span
+ v-html="svgIcon"
+ aria-hidden="true"
+ :aria-label="stage.title">
+ </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"
+ aria-labelledby="stageDropdown">
+
+ <li
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu">
+
+ <loading-icon v-if="isLoading"/>
+
+ <ul
+ v-else
+ v-html="dropdownContent">
+ </ul>
+ </li>
+ </ul>
+ </div>
+</script>
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..188f74cc705
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/time_ago.js
@@ -0,0 +1,98 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+ props: {
+ finishedTime: {
+ type: String,
+ required: true,
+ },
+
+ duration: {
+ type: Number,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ iconTimerSvg,
+ };
+ },
+
+ updated() {
+ $(this.$refs.tooltip).tooltip('fixTitle');
+ },
+
+ computed: {
+ hasDuration() {
+ return this.duration > 0;
+ },
+
+ hasFinishedTime() {
+ return this.finishedTime !== '';
+ },
+
+ localTimeFinished() {
+ return gl.utils.formatDate(this.finishedTime);
+ },
+
+ durationFormated() {
+ const date = new Date(this.duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ // left pad
+ if (hh < 10) {
+ hh = `0${hh}`;
+ }
+ if (mm < 10) {
+ mm = `0${mm}`;
+ }
+ if (ss < 10) {
+ ss = `0${ss}`;
+ }
+
+ return `${hh}:${mm}:${ss}`;
+ },
+
+ finishedTimeFormated() {
+ const timeAgo = gl.utils.getTimeago();
+
+ return timeAgo.format(this.finishedTime);
+ },
+ },
+
+ template: `
+ <td class="pipelines-time-ago">
+ <p
+ class="duration"
+ v-if="hasDuration">
+ <span
+ v-html="iconTimerSvg">
+ </span>
+ {{durationFormated}}
+ </p>
+
+ <p
+ class="finished-at"
+ v-if="hasFinishedTime">
+
+ <i
+ class="fa fa-calendar"
+ aria-hidden="true" />
+
+ <time
+ ref="tooltip"
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :title="localTimeFinished">
+ {{finishedTimeFormated}}
+ </time>
+ </p>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/pipelines/event_hub.js b/app/assets/javascripts/pipelines/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/pipelines/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/pipelines/graph_bundle.js b/app/assets/javascripts/pipelines/graph_bundle.js
new file mode 100644
index 00000000000..b7a6b5d8479
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graph_bundle.js
@@ -0,0 +1,10 @@
+import Vue from 'vue';
+import pipelineGraph from './components/graph/graph_component.vue';
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#js-pipeline-graph-vue',
+ components: {
+ pipelineGraph,
+ },
+ render: createElement => createElement('pipeline-graph'),
+}));
diff --git a/app/assets/javascripts/pipelines/index.js b/app/assets/javascripts/pipelines/index.js
new file mode 100644
index 00000000000..48f9181a8d9
--- /dev/null
+++ b/app/assets/javascripts/pipelines/index.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import PipelinesStore from './stores/pipelines_store';
+import PipelinesComponent from './pipelines';
+import '../vue_shared/vue_resource_interceptor';
+
+$(() => new Vue({
+ el: document.querySelector('#pipelines-list-vue'),
+
+ data() {
+ const store = new PipelinesStore();
+
+ return {
+ store,
+ };
+ },
+ components: {
+ 'vue-pipelines': PipelinesComponent,
+ },
+ template: `
+ <vue-pipelines :store="store" />
+ `,
+}));
diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js
new file mode 100644
index 00000000000..d6952d1ee5f
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -0,0 +1,295 @@
+import Visibility from 'visibilityjs';
+import PipelinesService from './services/pipelines_service';
+import eventHub from './event_hub';
+import pipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import tablePagination from '../vue_shared/components/table_pagination.vue';
+import emptyState from './components/empty_state.vue';
+import errorState from './components/error_state.vue';
+import navigationTabs from './components/navigation_tabs';
+import navigationControls from './components/nav_controls';
+import loadingIcon from '../vue_shared/components/loading_icon.vue';
+import Poll from '../lib/utils/poll';
+
+export default {
+ props: {
+ store: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ tablePagination,
+ pipelinesTableComponent,
+ emptyState,
+ errorState,
+ navigationTabs,
+ navigationControls,
+ loadingIcon,
+ },
+
+ data() {
+ const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
+
+ return {
+ endpoint: pipelinesData.endpoint,
+ cssClass: pipelinesData.cssClass,
+ helpPagePath: pipelinesData.helpPagePath,
+ newPipelinePath: pipelinesData.newPipelinePath,
+ canCreatePipeline: pipelinesData.canCreatePipeline,
+ allPath: pipelinesData.allPath,
+ pendingPath: pipelinesData.pendingPath,
+ runningPath: pipelinesData.runningPath,
+ finishedPath: pipelinesData.finishedPath,
+ branchesPath: pipelinesData.branchesPath,
+ tagsPath: pipelinesData.tagsPath,
+ hasCi: pipelinesData.hasCi,
+ ciLintPath: pipelinesData.ciLintPath,
+ state: this.store.state,
+ apiScope: 'all',
+ pagenum: 1,
+ isLoading: false,
+ hasError: false,
+ isMakingRequest: false,
+ updateGraphDropdown: false,
+ hasMadeRequest: false,
+ };
+ },
+
+ computed: {
+ canCreatePipelineParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
+ },
+
+ scope() {
+ const scope = gl.utils.getParameterByName('scope');
+ return scope === null ? 'all' : scope;
+ },
+
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
+
+ /**
+ * The empty state should only be rendered when the request is made to fetch all pipelines
+ * and none is returned.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderEmptyState() {
+ return !this.isLoading &&
+ !this.hasError &&
+ this.hasMadeRequest &&
+ !this.state.pipelines.length &&
+ (this.scope === 'all' || this.scope === null);
+ },
+
+ /**
+ * When a specific scope does not have pipelines we render a message.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderNoPipelinesMessage() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ this.scope !== 'all' &&
+ this.scope !== null;
+ },
+
+ shouldRenderTable() {
+ return !this.hasError &&
+ !this.isLoading && this.state.pipelines.length;
+ },
+
+ /**
+ * Pagination should only be rendered when there is more than one page.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderPagination() {
+ return !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage;
+ },
+
+ hasCiEnabled() {
+ return this.hasCi !== undefined;
+ },
+
+ paths() {
+ return {
+ allPath: this.allPath,
+ pendingPath: this.pendingPath,
+ finishedPath: this.finishedPath,
+ runningPath: this.runningPath,
+ branchesPath: this.branchesPath,
+ tagsPath: this.tagsPath,
+ };
+ },
+
+ pageParameter() {
+ return gl.utils.getParameterByName('page') || this.pagenum;
+ },
+
+ scopeParameter() {
+ return gl.utils.getParameterByName('scope') || this.apiScope;
+ },
+ },
+
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+
+ const poll = new Poll({
+ resource: this.service,
+ method: 'getPipelines',
+ data: { page: this.pageParameter, scope: this.scopeParameter },
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: this.setIsMakingRequest,
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ poll.makeRequest();
+ } else {
+ // If tab is not visible we need to make the first request so we don't show the empty
+ // state without knowing if there are any pipelines
+ this.fetchPipelines();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+
+ methods: {
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ */
+ change(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
+ },
+
+ fetchPipelines() {
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
+ .then(response => this.successCallback(response))
+ .catch(() => this.errorCallback());
+ }
+ },
+
+ successCallback(resp) {
+ const response = {
+ headers: resp.headers,
+ body: resp.json(),
+ };
+
+ this.store.storeCount(response.body.count);
+ this.store.storePipelines(response.body.pipelines);
+ this.store.storePagination(response.headers);
+
+ this.isLoading = false;
+ this.updateGraphDropdown = true;
+ this.hasMadeRequest = true;
+ },
+
+ errorCallback() {
+ this.hasError = true;
+ this.isLoading = false;
+ this.updateGraphDropdown = false;
+ },
+
+ setIsMakingRequest(isMakingRequest) {
+ this.isMakingRequest = isMakingRequest;
+
+ if (isMakingRequest) {
+ this.updateGraphDropdown = false;
+ }
+ },
+ },
+
+ template: `
+ <div :class="cssClass">
+
+ <div
+ class="top-area scrolling-tabs-container inner-page-scroll-tabs"
+ v-if="!isLoading && !shouldRenderEmptyState">
+ <div class="fade-left">
+ <i class="fa fa-angle-left" aria-hidden="true"></i>
+ </div>
+ <div class="fade-right">
+ <i class="fa fa-angle-right" aria-hidden="true"></i>
+ </div>
+ <navigation-tabs
+ :scope="scope"
+ :count="state.count"
+ :paths="paths" />
+
+ <navigation-controls
+ :new-pipeline-path="newPipelinePath"
+ :has-ci-enabled="hasCiEnabled"
+ :help-page-path="helpPagePath"
+ :ciLintPath="ciLintPath"
+ :can-create-pipeline="canCreatePipelineParsed " />
+ </div>
+
+ <div class="content-list pipelines">
+
+ <loading-icon
+ label="Loading Pipelines"
+ size="3"
+ v-if="isLoading"
+ />
+
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
+
+ <div
+ class="blank-state blank-state-no-icon"
+ v-if="shouldRenderNoPipelinesMessage">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
+
+ <div
+ class="table-holder"
+ v-if="shouldRenderTable">
+
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service"
+ :update-graph-dropdown="updateGraphDropdown"
+ />
+ </div>
+
+ <table-pagination
+ v-if="shouldRenderPagination"
+ :pagenum="pagenum"
+ :change="change"
+ :count="state.count.all"
+ :pageInfo="state.pageInfo"
+ />
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/pipelines/services/pipeline_service.js b/app/assets/javascripts/pipelines/services/pipeline_service.js
new file mode 100644
index 00000000000..f1cc60c1ee0
--- /dev/null
+++ b/app/assets/javascripts/pipelines/services/pipeline_service.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelineService {
+ constructor(endpoint) {
+ this.pipeline = Vue.resource(endpoint);
+ }
+
+ getPipeline() {
+ return this.pipeline.get();
+ }
+}
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
new file mode 100644
index 00000000000..b21f84b4545
--- /dev/null
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -0,0 +1,45 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelinesService {
+
+ /**
+ * Commits and merge request endpoints need to be requested with `.json`.
+ *
+ * The url provided to request the pipelines in the new merge request
+ * page already has `.json`.
+ *
+ * @param {String} root
+ */
+ constructor(root) {
+ let endpoint;
+
+ if (root.indexOf('.json') === -1) {
+ endpoint = `${root}.json`;
+ } else {
+ endpoint = root;
+ }
+
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ getPipelines(data = {}) {
+ const { scope, page } = data;
+ return this.pipelines.get({ scope, page });
+ }
+
+ /**
+ * Post request for all pipelines actions.
+ * Endpoint content type needs to be:
+ * `Content-Type:application/x-www-form-urlencoded`
+ *
+ * @param {String} endpoint
+ * @return {Promise}
+ */
+ postAction(endpoint) {
+ return Vue.http.post(`${endpoint}.json`);
+ }
+}
diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js
new file mode 100644
index 00000000000..86ab50d8f1e
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js
@@ -0,0 +1,11 @@
+export default class PipelineStore {
+ constructor() {
+ this.state = {};
+
+ this.state.graph = [];
+ }
+
+ storeGraph(graph = []) {
+ this.state.graph = graph;
+ }
+}
diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
new file mode 100644
index 00000000000..ffefe0192f2
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
@@ -0,0 +1,30 @@
+export default class PipelinesStore {
+ constructor() {
+ this.state = {};
+
+ this.state.pipelines = [];
+ this.state.count = {};
+ this.state.pageInfo = {};
+ }
+
+ storePipelines(pipelines = []) {
+ this.state.pipelines = pipelines;
+ }
+
+ storeCount(count = {}) {
+ this.state.count = count;
+ }
+
+ storePagination(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
+ }
+}