summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/common_vue.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js157
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js48
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js228
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js135
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js24
6 files changed, 598 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/common_vue.js b/app/assets/javascripts/vue_shared/common_vue.js
new file mode 100644
index 00000000000..eb2a6071fda
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/common_vue.js
@@ -0,0 +1,6 @@
+import Vue from 'vue';
+import './vue_resource_interceptor';
+
+if (process.env.NODE_ENV !== 'production') {
+ Vue.config.productionTip = false;
+}
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
new file mode 100644
index 00000000000..fb68abd95a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -0,0 +1,157 @@
+import commitIconSvg from 'icons/_icon_commit.svg';
+
+export default {
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ data() {
+ return { commitIconSvg };
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
+ </div>
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
new file mode 100644
index 00000000000..afd8d7acf6b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -0,0 +1,48 @@
+import PipelinesTableRowComponent from './pipelines_table_row';
+
+/**
+ * Pipelines Table Component.
+ *
+ * Given an array of objects, renders a table.
+ */
+export default {
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'pipelines-table-row-component': PipelinesTableRowComponent,
+ },
+
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="js-pipeline-status pipeline-status">Status</th>
+ <th class="js-pipeline-info pipeline-info">Pipeline</th>
+ <th class="js-pipeline-commit pipeline-commit">Commit</th>
+ <th class="js-pipeline-stages pipeline-stages">Stages</th>
+ <th class="js-pipeline-date pipeline-date"></th>
+ <th class="js-pipeline-actions pipeline-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :service="service"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
new file mode 100644
index 00000000000..f5b3cb9214e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -0,0 +1,228 @@
+/* eslint-disable no-param-reassign */
+
+import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
+import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
+import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
+import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import CommitComponent from './commit';
+
+/**
+ * Pipeline table row.
+ *
+ * Given the received object renders a table row in the pipelines' table.
+ */
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'async-button-component': AsyncButtonComponent,
+ 'pipelines-actions-component': PipelinesActionsComponent,
+ 'pipelines-artifacts-component': PipelinesArtifactsComponent,
+ 'commit-component': CommitComponent,
+ 'dropdown-stage': PipelinesStageComponent,
+ 'pipeline-url': PipelinesUrlComponent,
+ 'status-scope': PipelinesStatusComponent,
+ 'time-ago': PipelinesTimeagoComponent,
+ },
+
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
+
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline &&
+ this.pipeline.commit &&
+ this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
+
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ });
+ }
+ }
+
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ if (this.pipeline &&
+ this.pipeline.commit) {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
+
+ return commitAuthorInformation;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref &&
+ this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ accumulator[prop] = this.pipeline.ref[prop];
+ }
+ return accumulator;
+ }, {});
+ }
+
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope :pipeline="pipeline"/>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </td>
+
+ <td class="stage-cell">
+ <div class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.details.stages.length > 0"
+ v-for="stage in pipeline.details.stages">
+ <dropdown-stage :stage="stage"/>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline"/>
+
+ <td class="pipeline-actions">
+ <div class="pull-right btn-group">
+ <pipelines-actions-component
+ v-if="pipeline.details.manual_actions.length"
+ :actions="pipeline.details.manual_actions"
+ :service="service" />
+
+ <pipelines-artifacts-component
+ v-if="pipeline.details.artifacts.length"
+ :artifacts="pipeline.details.artifacts" />
+
+ <async-button-component
+ v-if="pipeline.flags.retryable"
+ :service="service"
+ :endpoint="pipeline.retry_path"
+ css-class="js-pipelines-retry-button btn-default btn-retry"
+ title="Retry"
+ icon="repeat" />
+
+ <async-button-component
+ v-if="pipeline.flags.cancelable"
+ :service="service"
+ :endpoint="pipeline.cancel_path"
+ css-class="js-pipelines-cancel-button btn-remove"
+ title="Cancel"
+ icon="remove"
+ confirm-action-message="Are you sure you want to cancel this pipeline?" />
+ </div>
+ </td>
+ </tr>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
new file mode 100644
index 00000000000..ebb14912b00
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -0,0 +1,135 @@
+const PAGINATION_UI_BUTTON_LIMIT = 4;
+const UI_LIMIT = 6;
+const SPREAD = '...';
+const PREV = 'Prev';
+const NEXT = 'Next';
+const FIRST = '« First';
+const LAST = 'Last »';
+
+export default {
+ props: {
+ /**
+ This function will take the information given by the pagination component
+
+ Here is an example `change` method:
+
+ change(pagenum) {
+ gl.utils.visitUrl(`?page=${pagenum}`);
+ },
+ */
+ change: {
+ type: Function,
+ required: true,
+ },
+
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ changePage(e) {
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages);
+ break;
+ case NEXT:
+ this.change(nextPage);
+ break;
+ case PREV:
+ this.change(previousPage);
+ break;
+ case FIRST:
+ this.change(1);
+ break;
+ default:
+ this.change(+text);
+ break;
+ }
+ },
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
+
+ if (page > 1) items.push({ title: FIRST });
+
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
+
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+
+ for (let i = start; i <= end; i += 1) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
+
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
+
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
+
+ if (total - page >= 1) items.push({ title: LAST, last: true });
+
+ return items;
+ },
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
new file mode 100644
index 00000000000..d5f87588c28
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+// Maintain a global counter for active requests
+// see: spec/support/wait_for_vue_resource.rb
+Vue.http.interceptors.push((request, next) => {
+ window.activeVueResources = window.activeVueResources || 0;
+ window.activeVueResources += 1;
+
+ next(() => {
+ window.activeVueResources -= 1;
+ });
+});
+
+// Inject CSRF token so we don't break any tests.
+Vue.http.interceptors.push((request, next) => {
+ if ($.rails) {
+ // eslint-disable-next-line no-param-reassign
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+ next();
+});