summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/api.js23
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue4
-rw-r--r--app/assets/javascripts/boards/components/board.js2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js9
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue112
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js7
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue12
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue64
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js5
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js16
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue14
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue7
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue4
-rw-r--r--app/assets/javascripts/ide/components/external_link.vue41
-rw-r--r--app/assets/javascripts/ide/components/ide.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue84
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue15
-rw-r--r--app/assets/javascripts/ide/components/jobs/item.vue46
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue44
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue108
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue26
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue65
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue146
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue2
-rw-r--r--app/assets/javascripts/ide/constants.js4
-rw-r--r--app/assets/javascripts/ide/ide_router.js2
-rw-r--r--app/assets/javascripts/ide/index.js10
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js1
-rw-r--r--app/assets/javascripts/ide/stores/actions.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js74
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js6
-rw-r--r--app/assets/javascripts/ide/stores/index.js25
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js11
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js25
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/constants.js10
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/index.js10
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js26
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/state.js8
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js89
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/constants.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js21
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js73
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/utils.js7
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js9
-rw-r--r--app/assets/javascripts/ide/stores/state.js2
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js20
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/job.js2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue10
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js5
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/locale/index.js6
-rw-r--r--app/assets/javascripts/locale/sprintf.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js4
-rw-r--r--app/assets/javascripts/notes/constants.js1
-rw-r--r--app/assets/javascripts/notes/stores/actions.js69
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js108
-rw-r--r--app/assets/javascripts/notes/stores/getters.js4
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/new/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js9
-rw-r--r--app/assets/javascripts/pages/sessions/new/oauth_remember_me.js2
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js31
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue3
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js71
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue142
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue201
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue116
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js11
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/index.js88
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js95
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js3
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js18
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js28
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js13
-rw-r--r--app/assets/javascripts/raven/raven_config.js2
-rw-r--r--app/assets/javascripts/registry/stores/actions.js20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue)6
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tab.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tabs.js64
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js4
-rw-r--r--app/assets/javascripts/vue_shared/translate.js6
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss53
-rw-r--r--app/assets/stylesheets/errors.scss120
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss10
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss19
-rw-r--r--app/assets/stylesheets/framework/header.scss6
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss15
-rw-r--r--app/assets/stylesheets/framework/modal.scss7
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss13
-rw-r--r--app/assets/stylesheets/framework/snippets.scss15
-rw-r--r--app/assets/stylesheets/framework/terms.scss8
-rw-r--r--app/assets/stylesheets/framework/typography.scss27
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss1
-rw-r--r--app/assets/stylesheets/pages/clusters.scss6
-rw-r--r--app/assets/stylesheets/pages/labels.scss8
-rw-r--r--app/assets/stylesheets/pages/members.scss38
-rw-r--r--app/assets/stylesheets/pages/profile.scss2
-rw-r--r--app/assets/stylesheets/pages/repo.scss146
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
-rw-r--r--app/assets/stylesheets/print.scss5
130 files changed, 2692 insertions, 622 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index ce1069276ab..000938e475f 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -11,6 +11,7 @@ const Api = {
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+ mergeRequestsPath: '/api/:version/merge_requests',
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
@@ -24,8 +25,6 @@ const Api = {
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
- pipelinesPath: '/api/:version/projects/:id/pipelines',
- pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -109,6 +108,12 @@ const Api = {
return axios.get(url);
},
+ mergeRequests(params = {}) {
+ const url = Api.buildUrl(Api.mergeRequestsPath);
+
+ return axios.get(url, { params });
+ },
+
mergeRequestChanges(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestChangesPath)
.replace(':id', encodeURIComponent(projectPath))
@@ -238,20 +243,6 @@ const Api = {
});
},
- pipelines(projectPath, params = {}) {
- const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath));
-
- return axios.get(url, { params });
- },
-
- pipelineJobs(projectPath, pipelineId, params = {}) {
- const url = Api.buildUrl(this.pipelineJobsPath)
- .replace(':id', encodeURIComponent(projectPath))
- .replace(':pipeline_id', pipelineId);
-
- return axios.get(url, { params });
- },
-
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index ae942b2c1a7..5975cb9669e 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -160,7 +160,7 @@ export default {
@input="debouncedPreview"
/>
<span
- class="help-block"
+ class="form-text text-muted"
v-html="helpText"
></span>
</div>
@@ -176,7 +176,7 @@ export default {
@input="debouncedPreview"
/>
<span
- class="help-block"
+ class="form-text text-muted"
v-html="helpText"
></span>
</div>
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index bea818010a4..ac06d79fb60 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
import $ from 'jquery';
-import Sortable from 'vendor/Sortable';
+import Sortable from 'sortablejs';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue';
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 0d03c1c419c..84a7f277227 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,5 +1,5 @@
<script>
-import Sortable from 'vendor/Sortable';
+import Sortable from 'sortablejs';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index eb8a66975ee..1e5f2383223 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
template: `
<section class="empty-state">
<div class="row">
- <div class="col-xs-12 col-sm-6 order-sm-last">
+ <div class="col-12 col-md-6 order-md-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div>
- <div class="col-xs-12 col-sm-6 order-sm-first">
+ <div class="col-12 col-md-6 order-md-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index 6c662432037..f86896d2178 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -1,5 +1,3 @@
-/* global ListIssue */
-
import Vue from 'vue';
import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store';
@@ -56,8 +54,11 @@ gl.issueBoards.ModalList = Vue.extend({
scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
- if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
- && currentPage === this.page) {
+ if (
+ this.scrollTop() > this.scrollHeight() - 100 &&
+ !this.loadingNewPage &&
+ currentPage === this.page
+ ) {
this.loadingNewPage = true;
this.page += 1;
}
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 371774098b9..eb335f352d3 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,71 +1,69 @@
<script>
- /* global ListIssue */
+import $ from 'jquery';
+import _ from 'underscore';
+import eventHub from '../eventhub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import Api from '../../api';
- import $ from 'jquery';
- import _ from 'underscore';
- import eventHub from '../eventhub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import Api from '../../api';
-
- export default {
- name: 'BoardProjectSelect',
- components: {
- loadingIcon,
- },
- props: {
- groupId: {
- type: Number,
- required: true,
- default: 0,
- },
+export default {
+ name: 'BoardProjectSelect',
+ components: {
+ loadingIcon,
+ },
+ props: {
+ groupId: {
+ type: Number,
+ required: true,
+ default: 0,
},
- data() {
- return {
- loading: true,
- selectedProject: {},
- };
+ },
+ data() {
+ return {
+ loading: true,
+ selectedProject: {},
+ };
+ },
+ computed: {
+ selectedProjectName() {
+ return this.selectedProject.name || 'Select a project';
},
- computed: {
- selectedProjectName() {
- return this.selectedProject.name || 'Select a project';
+ },
+ mounted() {
+ $(this.$refs.projectsDropdown).glDropdown({
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name_with_namespace'],
},
- },
- mounted() {
- $(this.$refs.projectsDropdown).glDropdown({
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name_with_namespace'],
- },
- clicked: ({ $el, e }) => {
- e.preventDefault();
- this.selectedProject = {
- id: $el.data('project-id'),
- name: $el.data('project-name'),
- };
- eventHub.$emit('setSelectedProject', this.selectedProject);
- },
- selectable: true,
- data: (term, callback) => {
- this.loading = true;
- return Api.groupProjects(this.groupId, term, (projects) => {
- this.loading = false;
- callback(projects);
- });
- },
- renderRow(project) {
- return `
+ clicked: ({ $el, e }) => {
+ e.preventDefault();
+ this.selectedProject = {
+ id: $el.data('project-id'),
+ name: $el.data('project-name'),
+ };
+ eventHub.$emit('setSelectedProject', this.selectedProject);
+ },
+ selectable: true,
+ data: (term, callback) => {
+ this.loading = true;
+ return Api.groupProjects(this.groupId, term, projects => {
+ this.loading = false;
+ callback(projects);
+ });
+ },
+ renderRow(project) {
+ return `
<li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
`;
- },
- text: project => project.name,
- });
- },
- };
+ },
+ text: project => project.name,
+ });
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 01aec4f36af..e42a3632e79 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -31,6 +31,7 @@ export default class Clusters {
installHelmPath,
installIngressPath,
installRunnerPath,
+ installJupyterPath,
installPrometheusPath,
managePrometheusPath,
clusterStatus,
@@ -51,6 +52,7 @@ export default class Clusters {
installIngressEndpoint: installIngressPath,
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
+ installJupyterEndpoint: installJupyterPath,
});
this.installApplication = this.installApplication.bind(this);
@@ -209,11 +211,12 @@ export default class Clusters {
}
}
- installApplication(appId) {
+ installApplication(data) {
+ const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null);
- this.service.installApplication(appId)
+ this.service.installApplication(appId, data.params)
.then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
})
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index fae580c091b..98c0b9c22a8 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -52,6 +52,11 @@
type: String,
required: false,
},
+ installApplicationRequestParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
rowJsClass() {
@@ -109,7 +114,10 @@
},
methods: {
installClicked() {
- eventHub.$emit('installApplication', this.id);
+ eventHub.$emit('installApplication', {
+ id: this.id,
+ params: this.installApplicationRequestParams,
+ });
},
},
};
@@ -179,7 +187,7 @@
role="row"
>
<div
- class="alert alert-danger alert-block append-bottom-0"
+ class="alert alert-danger alert-block append-bottom-0 clusters-error-alert"
role="gridcell"
>
<div>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index bb5fcea648d..9d6be555a2c 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -121,6 +121,12 @@ export default {
false,
);
},
+ jupyterInstalled() {
+ return this.applications.jupyter.status === APPLICATION_INSTALLED;
+ },
+ jupyterHostname() {
+ return this.applications.jupyter.hostname;
+ },
},
};
</script>
@@ -278,11 +284,67 @@ export default {
applications to production.`) }}
</div>
</application-row>
+ <application-row
+ id="jupyter"
+ :title="applications.jupyter.title"
+ title-link="https://jupyterhub.readthedocs.io/en/stable/"
+ :status="applications.jupyter.status"
+ :status-reason="applications.jupyter.statusReason"
+ :request-status="applications.jupyter.requestStatus"
+ :request-reason="applications.jupyter.requestReason"
+ :install-application-request-params="{ hostname: applications.jupyter.hostname }"
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
+ manages, and proxies multiple instances of the single-user
+ Jupyter notebook server. JupyterHub can be used to serve
+ notebooks to a class of students, a corporate data science group,
+ or a scientific research group.`) }}
+ </p>
+
+ <template v-if="ingressExternalIp">
+ <div class="form-group">
+ <label for="jupyter-hostname">
+ {{ s__('ClusterIntegration|Jupyter Hostname') }}
+ </label>
+
+ <div class="input-group">
+ <input
+ type="text"
+ class="form-control js-hostname"
+ v-model="applications.jupyter.hostname"
+ :readonly="jupyterInstalled"
+ />
+ <span
+ class="input-group-btn"
+ >
+ <clipboard-button
+ :text="jupyterHostname"
+ :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
+ class="js-clipboard-btn"
+ />
+ </span>
+ </div>
+ </div>
+ <p v-if="ingressInstalled">
+ {{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
+ If you do so, point hostname to Ingress IP Address from above.`) }}
+ <a
+ :href="ingressDnsHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+ </template>
+ </div>
+ </application-row>
<!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
-->
- <!-- Add GitLab Runner row, all other plumbing is complete -->
</div>
</div>
</section>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index b7179f52bb3..371f71fde44 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -11,3 +11,4 @@ export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
+export const JUPYTER = 'jupyter';
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index 13468578f4f..a7d82292ba9 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -8,6 +8,7 @@ export default class ClusterService {
ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
+ jupyter: this.options.installJupyterEndpoint,
};
}
@@ -15,8 +16,8 @@ export default class ClusterService {
return axios.get(this.options.endpoint);
}
- installApplication(appId) {
- return axios.post(this.appInstallEndpointMap[appId]);
+ installApplication(appId, params) {
+ return axios.post(this.appInstallEndpointMap[appId], params);
}
static updateCluster(endpoint, data) {
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 348bbec3b25..3a4ac09f67c 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,5 @@
import { s__ } from '../../locale';
-import { INGRESS } from '../constants';
+import { INGRESS, JUPYTER } from '../constants';
export default class ClusterStore {
constructor() {
@@ -38,6 +38,14 @@ export default class ClusterStore {
requestStatus: null,
requestReason: null,
},
+ jupyter: {
+ title: s__('ClusterIntegration|JupyterHub'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
};
}
@@ -83,6 +91,12 @@ export default class ClusterStore {
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
+ } else if (appId === JUPYTER) {
+ this.state.applications.jupyter.hostname =
+ serverAppEntry.hostname ||
+ (this.state.applications.ingress.externalIp
+ ? `jupyter.${this.state.applications.ingress.externalIp}.xip.io`
+ : '');
}
});
}
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 05dbc1410de..6efcad6adea 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -20,6 +21,13 @@ export default {
},
methods: {
...mapActions(['updateActivityBarView']),
+ changedActivityView(e, view) {
+ e.currentTarget.blur();
+
+ this.updateActivityBarView(view);
+
+ $(e.currentTarget).tooltip('hide');
+ },
},
activityBarViews,
};
@@ -54,7 +62,7 @@ export default {
:class="{
active: currentActivityView === $options.activityBarViews.edit
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.edit)"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.edit)"
:title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')"
>
@@ -73,7 +81,7 @@ export default {
:class="{
active: currentActivityView === $options.activityBarViews.review
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.review)"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.review)"
:title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')"
>
@@ -92,7 +100,7 @@ export default {
:class="{
active: currentActivityView === $options.activityBarViews.commit
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.commit)"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.commit)"
:title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')"
>
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 1cec84706fc..a4e06bbbe3c 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon} pull-left`;
+ return `multi-${this.changedIcon} float-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 81961fe3c57..e2b42ab2642 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -126,7 +126,6 @@ export default {
</div>
<form
v-if="!isCompact"
- class="form-horizontal"
@submit.prevent.stop="commitChanges"
ref="formEl"
>
@@ -144,14 +143,14 @@ export default {
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
- container-class="btn btn-success btn-sm pull-left"
+ container-class="btn btn-success btn-sm float-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
- class="btn btn-default btn-sm pull-right"
+ class="btn btn-default btn-sm float-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
@@ -159,7 +158,7 @@ export default {
<button
v-else
type="button"
- class="btn btn-default btn-sm pull-right"
+ class="btn btn-default btn-sm float-right"
@click="toggleIsSmall"
>
{{ __('Collapse') }}
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index c3ac18bfb83..1325fc993b2 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -120,7 +120,7 @@ export default {
</ul>
<p
v-else
- class="multi-file-commit-list help-block"
+ class="multi-file-commit-list form-text text-muted"
>
{{ __('No changes') }}
</p>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index dcd934f76b7..0ac0af2feaa 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -54,7 +54,7 @@ export default {
placement: 'top',
content: sprintf(
__(`
- The character highligher helps you keep the subject line to %{titleLength} characters
+ The character highlighter helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
@@ -80,7 +80,7 @@ export default {
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
- class="help-block prepend-left-10"
+ class="form-text text-muted prepend-left-10"
>
<icon
name="question"
diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue
new file mode 100644
index 00000000000..cf3316a8179
--- /dev/null
+++ b/app/assets/javascripts/ide/components/external_link.vue
@@ -0,0 +1,41 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showButtons() {
+ return this.file.permalink;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="showButtons"
+ class="pull-right ide-btn-group"
+ >
+ <a
+ :href="file.permalink"
+ target="_blank"
+ :title="s__('IDE|Open in file view')"
+ rel="noopener noreferrer"
+ >
+ <span class="vertical-align-middle">Open in file view</span>
+ <icon
+ name="external-link"
+ css-classes="vertical-align-middle space-right"
+ :size="16"
+ />
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 2c184ea726c..f5f832521c5 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -6,6 +6,7 @@ import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
+import RightPane from './panes/right.vue';
const originalStopCallback = Mousetrap.stopCallback;
@@ -16,6 +17,7 @@ export default {
IdeStatusBar,
RepoEditor,
FindFile,
+ RightPane,
},
computed: {
...mapState([
@@ -25,6 +27,7 @@ export default {
'currentMergeRequestId',
'fileFindVisible',
'emptyStateSvgPath',
+ 'currentProjectId',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
@@ -122,6 +125,9 @@ export default {
</div>
</template>
</div>
+ <right-pane
+ v-if="currentProjectId"
+ />
</div>
<ide-status-bar :file="activeFile"/>
</article>
diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue
deleted file mode 100644
index 30b00abf6ed..00000000000
--- a/app/assets/javascripts/ide/components/ide_file_buttons.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<script>
-import { __ } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- Icon,
- },
- directives: {
- tooltip,
- },
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- showButtons() {
- return (
- this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
- );
- },
- rawDownloadButtonLabel() {
- return this.file.binary ? __('Download') : __('Raw');
- },
- },
-};
-</script>
-
-<template>
- <div
- v-if="showButtons"
- class="float-right ide-btn-group"
- >
- <a
- v-tooltip
- v-if="!file.binary"
- :href="file.blamePath"
- :title="__('Blame')"
- class="btn btn-sm btn-transparent blame"
- >
- <icon
- name="blame"
- :size="16"
- />
- </a>
- <a
- v-tooltip
- :href="file.commitsPath"
- :title="__('History')"
- class="btn btn-sm btn-transparent history"
- >
- <icon
- name="history"
- :size="16"
- />
- </a>
- <a
- v-tooltip
- :href="file.permalink"
- :title="__('Permalink')"
- class="btn btn-sm btn-transparent permalink"
- >
- <icon
- name="link"
- :size="16"
- />
- </a>
- <a
- v-tooltip
- :href="file.rawPath"
- target="_blank"
- class="btn btn-sm btn-transparent prepend-left-10 raw"
- rel="noopener noreferrer"
- :title="rawDownloadButtonLabel">
- <icon
- name="download"
- :size="16"
- />
- </a>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 6f60cfbf184..368a2995ed9 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -31,6 +31,7 @@ export default {
computed: {
...mapState(['currentBranchId', 'currentProjectId']),
...mapGetters(['currentProject', 'lastCommit']),
+ ...mapState('pipelines', ['latestPipeline']),
},
watch: {
lastCommit() {
@@ -51,14 +52,14 @@ export default {
}
},
methods: {
- ...mapActions(['pipelinePoll', 'stopPipelinePolling']),
+ ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() {
this.intervalId = setInterval(() => {
this.commitAgeUpdate();
}, 1000);
},
initPipelinePolling() {
- this.pipelinePoll();
+ this.fetchLatestPipeline();
this.isPollingInitialized = true;
},
commitAgeUpdate() {
@@ -81,18 +82,18 @@ export default {
>
<span
class="ide-status-pipeline"
- v-if="lastCommit.pipeline && lastCommit.pipeline.details"
+ v-if="latestPipeline && latestPipeline.details"
>
<ci-icon
- :status="lastCommit.pipeline.details.status"
+ :status="latestPipeline.details.status"
v-tooltip
- :title="lastCommit.pipeline.details.status.text"
+ :title="latestPipeline.details.status.text"
/>
Pipeline
<a
class="monospace"
- :href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a>
- {{ lastCommit.pipeline.details.status.text }}
+ :href="latestPipeline.details.status.details_path">#{{ latestPipeline.id }}</a>
+ {{ latestPipeline.details.status.text }}
for
</span>
diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue
new file mode 100644
index 00000000000..c33936021d4
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/item.vue
@@ -0,0 +1,46 @@
+<script>
+import Icon from '../../../vue_shared/components/icon.vue';
+import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+
+export default {
+ components: {
+ Icon,
+ CiIcon,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ jobId() {
+ return `#${this.job.id}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="ide-job-item">
+ <ci-icon
+ :status="job.status"
+ :borderless="true"
+ :size="24"
+ />
+ <span class="prepend-left-8">
+ {{ job.name }}
+ <a
+ :href="job.path"
+ target="_blank"
+ class="ide-external-link"
+ >
+ {{ jobId }}
+ <icon
+ name="external-link"
+ :size="12"
+ />
+ </a>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
new file mode 100644
index 00000000000..bdd0364c9b9
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -0,0 +1,44 @@
+<script>
+import { mapActions } from 'vuex';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Stage from './stage.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ Stage,
+ },
+ props: {
+ stages: {
+ type: Array,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <loading-icon
+ v-if="loading && !stages.length"
+ class="prepend-top-default"
+ size="2"
+ />
+ <template v-else>
+ <stage
+ v-for="stage in stages"
+ :key="stage.id"
+ :stage="stage"
+ @fetch="fetchJobs"
+ @toggleCollapsed="toggleStageCollapsed"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
new file mode 100644
index 00000000000..5b24bb1f5a7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -0,0 +1,108 @@
+<script>
+import tooltip from '../../../vue_shared/directives/tooltip';
+import Icon from '../../../vue_shared/components/icon.vue';
+import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Item from './item.vue';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ CiIcon,
+ LoadingIcon,
+ Item,
+ },
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showTooltip: false,
+ };
+ },
+ computed: {
+ collapseIcon() {
+ return this.stage.isCollapsed ? 'angle-left' : 'angle-down';
+ },
+ showLoadingIcon() {
+ return this.stage.isLoading && !this.stage.jobs.length;
+ },
+ jobsCount() {
+ return this.stage.jobs.length;
+ },
+ },
+ mounted() {
+ const { stageTitle } = this.$refs;
+
+ this.showTooltip = stageTitle.scrollWidth > stageTitle.offsetWidth;
+
+ this.$emit('fetch', this.stage);
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed', this.stage.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-stage card prepend-top-default"
+ >
+ <div
+ class="card-header"
+ :class="{
+ 'border-bottom-0': stage.isCollapsed
+ }"
+ @click="toggleCollapsed"
+ >
+ <ci-icon
+ :status="stage.status"
+ :size="24"
+ />
+ <strong
+ v-tooltip="showTooltip"
+ :title="showTooltip ? stage.name : null"
+ data-container="body"
+ class="prepend-left-8 ide-stage-title"
+ ref="stageTitle"
+ >
+ {{ stage.name }}
+ </strong>
+ <div
+ v-if="!stage.isLoading || stage.jobs.length"
+ class="append-right-8 prepend-left-4"
+ >
+ <span class="badge badge-pill">
+ {{ jobsCount }}
+ </span>
+ </div>
+ <icon
+ :name="collapseIcon"
+ css-classes="ide-stage-collapse-icon"
+ />
+ </div>
+ <div
+ class="card-body"
+ v-show="!stage.isCollapsed"
+ >
+ <loading-icon
+ v-if="showLoadingIcon"
+ />
+ <template v-else>
+ <item
+ v-for="job in stage.jobs"
+ :key="job.id"
+ :job="job"
+ />
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index d83a90f71e1..dd2800179ff 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -72,21 +72,19 @@ export default {
<form
slot="body"
@submit.prevent="createEntryInStore"
- class="form-group row append-bottom-0"
+ class="form-group row"
>
- <fieldset class="form-group append-bottom-0">
- <label class="label-light col-form-label col-sm-3 ide-new-modal-label">
- {{ __('Name') }}
- </label>
- <div class="col-sm-9">
- <input
- type="text"
- class="form-control"
- v-model="entryName"
- ref="fieldName"
- />
- </div>
- </fieldset>
+ <label class="label-light col-form-label col-sm-3">
+ {{ __('Name') }}
+ </label>
+ <div class="col-sm-9">
+ <input
+ type="text"
+ class="form-control"
+ v-model="entryName"
+ ref="fieldName"
+ />
+ </div>
</form>
</deprecated-modal>
</template>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
new file mode 100644
index 00000000000..703c4a70cfa
--- /dev/null
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -0,0 +1,65 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import Icon from '../../../vue_shared/components/icon.vue';
+import { rightSidebarViews } from '../../constants';
+import PipelinesList from '../pipelines/list.vue';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ PipelinesList,
+ },
+ computed: {
+ ...mapState(['rightPane']),
+ },
+ methods: {
+ ...mapActions(['setRightPane']),
+ clickTab(e, view) {
+ e.target.blur();
+
+ this.setRightPane(view);
+ },
+ },
+ rightSidebarViews,
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel ide-right-sidebar"
+ >
+ <div
+ class="multi-file-commit-panel-inner"
+ v-if="rightPane"
+ >
+ <component :is="rightPane" />
+ </div>
+ <nav class="ide-activity-bar">
+ <ul class="list-unstyled">
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="__('Pipelines')"
+ class="ide-sidebar-link is-right"
+ :class="{
+ active: rightPane === $options.rightSidebarViews.pipelines
+ }"
+ type="button"
+ @click="clickTab($event, $options.rightSidebarViews.pipelines)"
+ >
+ <icon
+ :size="16"
+ name="pipeline"
+ />
+ </button>
+ </li>
+ </ul>
+ </nav>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
new file mode 100644
index 00000000000..06455fac439
--- /dev/null
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -0,0 +1,146 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import _ from 'underscore';
+import { sprintf, __ } from '../../../locale';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
+import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+import Tabs from '../../../vue_shared/components/tabs/tabs';
+import Tab from '../../../vue_shared/components/tabs/tab.vue';
+import EmptyState from '../../../pipelines/components/empty_state.vue';
+import JobsList from '../jobs/list.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ Icon,
+ CiIcon,
+ Tabs,
+ Tab,
+ JobsList,
+ EmptyState,
+ },
+ computed: {
+ ...mapState(['pipelinesEmptyStateSvgPath', 'links']),
+ ...mapGetters(['currentProject']),
+ ...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
+ ...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
+ ciLintText() {
+ return sprintf(
+ __('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'),
+ {
+ linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ showLoadingIcon() {
+ return this.isLoadingPipeline && this.latestPipeline === null;
+ },
+ },
+ created() {
+ this.fetchLatestPipeline();
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchLatestPipeline']),
+ },
+};
+</script>
+
+<template>
+ <div class="ide-pipeline">
+ <loading-icon
+ v-if="showLoadingIcon"
+ class="prepend-top-default"
+ size="2"
+ />
+ <template v-else-if="latestPipeline !== null">
+ <header
+ v-if="latestPipeline"
+ class="ide-tree-header ide-pipeline-header"
+ >
+ <ci-icon
+ :status="latestPipeline.details.status"
+ :size="24"
+ />
+ <span class="prepend-left-8">
+ <strong>
+ {{ __('Pipeline') }}
+ </strong>
+ <a
+ :href="latestPipeline.path"
+ target="_blank"
+ class="ide-external-link"
+ >
+ #{{ latestPipeline.id }}
+ <icon
+ name="external-link"
+ :size="12"
+ />
+ </a>
+ </span>
+ </header>
+ <empty-state
+ v-if="latestPipeline === false"
+ :help-page-path="links.ciHelpPagePath"
+ :empty-state-svg-path="pipelinesEmptyStateSvgPath"
+ :can-set-ci="true"
+ />
+ <div
+ v-else-if="latestPipeline.yamlError"
+ class="bs-callout bs-callout-danger"
+ >
+ <p class="append-bottom-0">
+ {{ __('Found errors in your .gitlab-ci.yml:') }}
+ </p>
+ <p class="append-bottom-0">
+ {{ latestPipeline.yamlError }}
+ </p>
+ <p
+ class="append-bottom-0"
+ v-html="ciLintText"
+ ></p>
+ </div>
+ <tabs
+ v-else
+ class="ide-pipeline-list"
+ >
+ <tab
+ :active="!pipelineFailed"
+ >
+ <template slot="title">
+ {{ __('Jobs') }}
+ <span
+ v-if="jobsCount"
+ class="badge badge-pill"
+ >
+ {{ jobsCount }}
+ </span>
+ </template>
+ <jobs-list
+ :loading="isLoadingJobs"
+ :stages="stages"
+ />
+ </tab>
+ <tab
+ :active="pipelineFailed"
+ >
+ <template slot="title">
+ {{ __('Failed Jobs') }}
+ <span
+ v-if="failedJobsCount"
+ class="badge badge-pill"
+ >
+ {{ failedJobsCount }}
+ </span>
+ </template>
+ <jobs-list
+ :loading="isLoadingJobs"
+ :stages="failedStages"
+ />
+ </tab>
+ </tabs>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index a281ecb95c8..93453989c08 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -6,12 +6,12 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer
import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
-import IdeFileButtons from './ide_file_buttons.vue';
+import ExternalLink from './external_link.vue';
export default {
components: {
ContentViewer,
- IdeFileButtons,
+ ExternalLink,
},
props: {
file: {
@@ -224,7 +224,7 @@ export default {
</a>
</li>
</ul>
- <ide-file-buttons
+ <external-link
:file="file"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 442697e1c80..f56aeced806 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -169,7 +169,7 @@ export default {
:show-tooltip="true"
:show-staged-icon="true"
:force-modified-icon="true"
- class="pull-right"
+ class="float-right"
/>
</span>
<new-dropdown
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 83fe22f40a4..33cd20caf52 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -20,3 +20,7 @@ export const viewerTypes = {
edit: 'editor',
diff: 'diff',
};
+
+export const rightSidebarViews = {
+ pipelines: 'pipelines-list',
+};
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index a21cec4e8d8..b52618f4fde 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -63,7 +63,7 @@ router.beforeEach((to, from, next) => {
.then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
- const baseSplit = to.params[0].split('/-/');
+ const baseSplit = (to.params[0] && to.params[0].split('/-/')) || [''];
const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0];
if (branchId) {
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index c5835cd3b06..2d74192e6b3 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { mapActions } from 'vuex';
import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue';
import store from './stores';
@@ -17,11 +18,18 @@ export function initIde(el) {
ide,
},
created() {
- this.$store.dispatch('setEmptyStateSvgs', {
+ this.setEmptyStateSvgs({
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
+ pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath,
});
+ this.setLinks({
+ ciHelpPagePath: el.dataset.ciHelpPagePath,
+ });
+ },
+ methods: {
+ ...mapActions(['setEmptyStateSvgs', 'setLinks']),
},
render(createElement) {
return createElement('ide');
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index b1e43a1e38c..e5149b1f3ad 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -1,4 +1,3 @@
-/* global monaco */
import Disposable from './disposable';
import eventHub from '../../eventhub';
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 1a98b42761e..3dc365eaead 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -169,6 +169,12 @@ export const burstUnusedSeal = ({ state, commit }) => {
}
};
+export const setRightPane = ({ commit }, view) => {
+ commit(types.SET_RIGHT_PANE, view);
+};
+
+export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 13aea91d8ba..74f9c112f5a 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -84,11 +84,11 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
});
};
-export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
};
-export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
const file = state.entries[path];
return new Promise((resolve, reject) => {
service
@@ -156,7 +156,7 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
}
};
-export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
+export const setFileViewMode = ({ commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index da73034fd7d..5ec9bd661bb 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -3,7 +3,7 @@ import service from '../../services';
import * as types from '../mutation_types';
export const getMergeRequestData = (
- { commit, state, dispatch },
+ { commit, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
@@ -32,7 +32,7 @@ export const getMergeRequestData = (
});
export const getMergeRequestChanges = (
- { commit, state, dispatch },
+ { commit, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
@@ -58,7 +58,7 @@ export const getMergeRequestChanges = (
});
export const getMergeRequestVersions = (
- { commit, state, dispatch },
+ { commit, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index cece9154c82..46af47d2f81 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,16 +1,9 @@
-import Visibility from 'visibilityjs';
import flash from '~/flash';
import { __ } from '~/locale';
import service from '../../services';
import * as types from '../mutation_types';
-import Poll from '../../../lib/utils/poll';
-let eTagPoll;
-
-export const getProjectData = (
- { commit, state, dispatch },
- { namespace, projectId, force = false } = {},
-) =>
+export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
@@ -40,10 +33,7 @@ export const getProjectData = (
}
});
-export const getBranchData = (
- { commit, state, dispatch },
- { projectId, branchId, force = false } = {},
-) =>
+export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
new Promise((resolve, reject) => {
if (
typeof state.projects[`${projectId}`] === 'undefined' ||
@@ -78,7 +68,7 @@ export const getBranchData = (
}
});
-export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) =>
+export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service
.getBranchData(projectId, branchId)
.then(({ data }) => {
@@ -91,61 +81,3 @@ export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId,
.catch(() => {
flash(__('Error loading last commit.'), 'alert', document, null, false, true);
});
-
-export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => {
- if (data.pipelines && data.pipelines.length) {
- const lastCommitHash =
- state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id;
- const lastCommitPipeline = data.pipelines.find(
- pipeline => pipeline.commit.id === lastCommitHash,
- );
- commit(types.SET_LAST_COMMIT_PIPELINE, {
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- pipeline: lastCommitPipeline || {},
- });
- }
-
- return data;
-};
-
-export const pipelinePoll = ({ getters, dispatch }) => {
- eTagPoll = new Poll({
- resource: service,
- method: 'lastCommitPipelines',
- data: {
- getters,
- },
- successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }),
- errorCallback: () => {
- flash(
- __('Something went wrong while fetching the latest pipeline status.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
- },
- });
-
- if (!Visibility.hidden()) {
- eTagPoll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- eTagPoll.restart();
- } else {
- eTagPoll.stop();
- }
- });
-};
-
-export const stopPipelinePolling = () => {
- eTagPoll.stop();
-};
-
-export const restartPipelinePolling = () => {
- eTagPoll.restart();
-};
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 6536be04f0a..cc5116413f7 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -5,7 +5,7 @@ import * as types from '../mutation_types';
import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
-export const toggleTreeOpen = ({ commit, dispatch }, path) => {
+export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
};
@@ -23,7 +23,7 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
}
};
-export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
+export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service
@@ -49,7 +49,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
-export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+export const getFiles = ({ state, commit }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId];
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 699710055e3..f8ce8a67ec0 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -6,16 +6,21 @@ import * as getters from './getters';
import mutations from './mutations';
import commitModule from './modules/commit';
import pipelines from './modules/pipelines';
+import mergeRequests from './modules/merge_requests';
Vue.use(Vuex);
-export default new Vuex.Store({
- state: state(),
- actions,
- mutations,
- getters,
- modules: {
- commit: commitModule,
- pipelines,
- },
-});
+export const createStore = () =>
+ new Vuex.Store({
+ state: state(),
+ actions,
+ mutations,
+ getters,
+ modules: {
+ commit: commitModule,
+ pipelines,
+ mergeRequests,
+ },
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index cd25c3060f2..0a0db4033c8 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -31,9 +31,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
const currentProject = rootState.projects[rootState.currentProjectId];
const commitStats = data.stats
? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
- additions: data.stats.additions, // eslint-disable-line indent
- deletions: data.stats.deletions, // eslint-disable-line indent
- }) // eslint-disable-line indent
+ additions: data.stats.additions, // eslint-disable-line indent-legacy
+ deletions: data.stats.deletions, // eslint-disable-line indent-legacy
+ }) // eslint-disable-line indent-legacy
: '';
const commitMsg = sprintf(
__('Your changes have been committed. Commit %{commitId} %{commitStats}'),
@@ -74,10 +74,7 @@ export const checkCommitStatus = ({ rootState }) =>
),
);
-export const updateFilesAfterCommit = (
- { commit, dispatch, state, rootState, rootGetters },
- { data },
-) => {
+export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
new file mode 100644
index 00000000000..d3050183bd3
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -0,0 +1,25 @@
+import { __ } from '../../../../locale';
+import Api from '../../../../api';
+import flash from '../../../../flash';
+import * as types from './mutation_types';
+
+export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS);
+export const receiveMergeRequestsError = ({ commit }) => {
+ flash(__('Error loading merge requests.'));
+ commit(types.RECEIVE_MERGE_REQUESTS_ERROR);
+};
+export const receiveMergeRequestsSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data);
+
+export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => {
+ dispatch('requestMergeRequests');
+ dispatch('resetMergeRequests');
+
+ Api.mergeRequests({ scope, state, search })
+ .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data))
+ .catch(() => dispatch('receiveMergeRequestsError'));
+};
+
+export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS);
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js
new file mode 100644
index 00000000000..64b7763f257
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js
@@ -0,0 +1,10 @@
+export const scopes = {
+ assignedToMe: 'assigned-to-me',
+ createdByMe: 'created-by-me',
+};
+
+export const states = {
+ opened: 'opened',
+ closed: 'closed',
+ merged: 'merged',
+};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
new file mode 100644
index 00000000000..04e7e0f08f1
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
@@ -0,0 +1,10 @@
+import state from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+export default {
+ namespaced: true,
+ state: state(),
+ actions,
+ mutations,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js
new file mode 100644
index 00000000000..0badddcbae7
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js
@@ -0,0 +1,5 @@
+export const REQUEST_MERGE_REQUESTS = 'REQUEST_MERGE_REQUESTS';
+export const RECEIVE_MERGE_REQUESTS_ERROR = 'RECEIVE_MERGE_REQUESTS_ERROR';
+export const RECEIVE_MERGE_REQUESTS_SUCCESS = 'RECEIVE_MERGE_REQUESTS_SUCCESS';
+
+export const RESET_MERGE_REQUESTS = 'RESET_MERGE_REQUESTS';
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
new file mode 100644
index 00000000000..98102a68e08
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
@@ -0,0 +1,26 @@
+/* eslint-disable no-param-reassign */
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_MERGE_REQUESTS](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_MERGE_REQUESTS_ERROR](state) {
+ state.isLoading = false;
+ },
+ [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) {
+ state.isLoading = false;
+ state.mergeRequests = data.map(mergeRequest => ({
+ id: mergeRequest.id,
+ iid: mergeRequest.iid,
+ title: mergeRequest.title,
+ projectId: mergeRequest.project_id,
+ projectPathWithNamespace: mergeRequest.web_url
+ .replace(`${gon.gitlab_url}/`, '')
+ .replace(`/merge_requests/${mergeRequest.iid}`, ''),
+ }));
+ },
+ [types.RESET_MERGE_REQUESTS](state) {
+ state.mergeRequests = [];
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
new file mode 100644
index 00000000000..2947b686c1c
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
@@ -0,0 +1,8 @@
+import { scopes, states } from './constants';
+
+export default () => ({
+ isLoading: false,
+ mergeRequests: [],
+ scope: scopes.assignedToMe,
+ state: states.opened,
+});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 07f7b201f2e..1ebe487263b 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -1,49 +1,80 @@
+import Visibility from 'visibilityjs';
+import axios from 'axios';
import { __ } from '../../../../locale';
-import Api from '../../../../api';
import flash from '../../../../flash';
+import Poll from '../../../../lib/utils/poll';
+import service from '../../../services';
import * as types from './mutation_types';
+let eTagPoll;
+
+export const clearEtagPoll = () => {
+ eTagPoll = null;
+};
+export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop();
+export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart();
+
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
-export const receiveLatestPipelineError = ({ commit }) => {
+export const receiveLatestPipelineError = ({ commit, dispatch }) => {
flash(__('There was an error loading latest pipeline'));
commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
+ dispatch('stopPipelinePolling');
+};
+export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipelines }) => {
+ let lastCommitPipeline = false;
+
+ if (pipelines && pipelines.length) {
+ const lastCommitHash = rootGetters.lastCommit && rootGetters.lastCommit.id;
+ lastCommitPipeline = pipelines.find(pipeline => pipeline.commit.id === lastCommitHash);
+ }
+
+ commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline);
};
-export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
- commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
-export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
+export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
+ if (eTagPoll) return;
+
dispatch('requestLatestPipeline');
- return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' })
- .then(({ data }) => {
- dispatch('receiveLatestPipelineSuccess', data.pop());
- })
- .catch(() => dispatch('receiveLatestPipelineError'));
+ eTagPoll = new Poll({
+ resource: service,
+ method: 'lastCommitPipelines',
+ data: { getters: rootGetters },
+ successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data),
+ errorCallback: () => dispatch('receiveLatestPipelineError'),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ eTagPoll.restart();
+ } else {
+ eTagPoll.stop();
+ }
+ });
};
-export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS);
-export const receiveJobsError = ({ commit }) => {
+export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
+export const receiveJobsError = ({ commit }, id) => {
flash(__('There was an error loading jobs'));
- commit(types.RECEIVE_JOBS_ERROR);
+ commit(types.RECEIVE_JOBS_ERROR, id);
};
-export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data);
+export const receiveJobsSuccess = ({ commit }, { id, data }) =>
+ commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
-export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => {
- dispatch('requestJobs');
+export const fetchJobs = ({ dispatch }, stage) => {
+ dispatch('requestJobs', stage.id);
- Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, {
- page,
- })
- .then(({ data, headers }) => {
- const nextPage = headers && headers['x-next-page'];
-
- dispatch('receiveJobsSuccess', data);
-
- if (nextPage) {
- dispatch('fetchJobs', nextPage);
- }
- })
- .catch(() => dispatch('receiveJobsError'));
+ axios
+ .get(stage.dropdownPath)
+ .then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
+ .catch(() => dispatch('receiveJobsError', stage.id));
};
+export const toggleStageCollapsed = ({ commit }, stageId) =>
+ commit(types.TOGGLE_STAGE_COLLAPSE, stageId);
+
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
new file mode 100644
index 00000000000..f5b96327e40
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
@@ -0,0 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
+export const states = {
+ failed: 'failed',
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
index d6c91f5b64d..f545453806f 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -1,7 +1,22 @@
+import { states } from './constants';
+
export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
-export const failedJobs = state =>
+export const pipelineFailed = state =>
+ state.latestPipeline && state.latestPipeline.details.status.text === states.failed;
+
+export const failedStages = state =>
+ state.stages.filter(stage => stage.status.text.toLowerCase() === states.failed).map(stage => ({
+ ...stage,
+ jobs: stage.jobs.filter(job => job.status.text.toLowerCase() === states.failed),
+ }));
+
+export const failedJobsCount = state =>
state.stages.reduce(
- (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
- [],
+ (acc, stage) => acc + stage.jobs.filter(j => j.status.text === states.failed).length,
+ 0,
);
+
+export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
index 6b5701670a6..3ddc8409c5b 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -5,3 +5,5 @@ export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCES
export const REQUEST_JOBS = 'REQUEST_JOBS';
export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
+
+export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index 2b16e57b386..745797e1ee5 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
+import { normalizeJob } from './utils';
export default {
[types.REQUEST_LATEST_PIPELINE](state) {
@@ -14,40 +15,52 @@ export default {
if (pipeline) {
state.latestPipeline = {
id: pipeline.id,
- status: pipeline.status,
+ path: pipeline.path,
+ commit: pipeline.commit,
+ details: {
+ status: pipeline.details.status,
+ },
+ yamlError: pipeline.yaml_errors,
};
+ state.stages = pipeline.details.stages.map((stage, i) => {
+ const foundStage = state.stages.find(s => s.id === i);
+ return {
+ id: i,
+ dropdownPath: stage.dropdown_path,
+ name: stage.name,
+ status: stage.status,
+ isCollapsed: foundStage ? foundStage.isCollapsed : false,
+ isLoading: foundStage ? foundStage.isLoading : false,
+ jobs: foundStage ? foundStage.jobs : [],
+ };
+ });
+ } else {
+ state.latestPipeline = false;
}
},
- [types.REQUEST_JOBS](state) {
- state.isLoadingJobs = true;
+ [types.REQUEST_JOBS](state, id) {
+ state.stages = state.stages.map(stage => ({
+ ...stage,
+ isLoading: stage.id === id ? true : stage.isLoading,
+ }));
},
- [types.RECEIVE_JOBS_ERROR](state) {
- state.isLoadingJobs = false;
+ [types.RECEIVE_JOBS_ERROR](state, id) {
+ state.stages = state.stages.map(stage => ({
+ ...stage,
+ isLoading: stage.id === id ? false : stage.isLoading,
+ }));
},
- [types.RECEIVE_JOBS_SUCCESS](state, jobs) {
- state.isLoadingJobs = false;
-
- state.stages = jobs.reduce((acc, job) => {
- let stage = acc.find(s => s.title === job.stage);
-
- if (!stage) {
- stage = {
- title: job.stage,
- jobs: [],
- };
-
- acc.push(stage);
- }
-
- stage.jobs = stage.jobs.concat({
- id: job.id,
- name: job.name,
- status: job.status,
- stage: job.stage,
- duration: job.duration,
- });
-
- return acc;
- }, state.stages);
+ [types.RECEIVE_JOBS_SUCCESS](state, { id, data }) {
+ state.stages = state.stages.map(stage => ({
+ ...stage,
+ isLoading: stage.id === id ? false : stage.isLoading,
+ jobs: stage.id === id ? data.latest_statuses.map(normalizeJob) : stage.jobs,
+ }));
+ },
+ [types.TOGGLE_STAGE_COLLAPSE](state, id) {
+ state.stages = state.stages.map(stage => ({
+ ...stage,
+ isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed,
+ }));
},
};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
index 6f22542aaea..0f83b315fff 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
@@ -1,5 +1,5 @@
export default () => ({
- isLoadingPipeline: false,
+ isLoadingPipeline: true,
isLoadingJobs: false,
latestPipeline: null,
stages: [],
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
new file mode 100644
index 00000000000..9f4b0d7d726
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
@@ -0,0 +1,7 @@
+// eslint-disable-next-line import/prefer-default-export
+export const normalizeJob = job => ({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ path: job.build_path,
+});
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 0a3f8d031c4..fbfb92105d6 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -6,6 +6,7 @@ export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
+export const SET_LINKS = 'SET_LINKS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
@@ -23,7 +24,6 @@ export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
-export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
@@ -66,3 +66,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
+
+export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index a257e2ef025..eeaa7cb0ec3 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -114,12 +114,13 @@ export default {
},
[types.SET_EMPTY_STATE_SVGS](
state,
- { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
+ { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath, pipelinesEmptyStateSvgPath },
) {
Object.assign(state, {
emptyStateSvgPath,
noChangesStateSvgPath,
committedStateSvgPath,
+ pipelinesEmptyStateSvgPath,
});
},
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
@@ -148,6 +149,14 @@ export default {
unusedSeal: false,
});
},
+ [types.SET_RIGHT_PANE](state, view) {
+ Object.assign(state, {
+ rightPane: state.rightPane === view ? null : view,
+ });
+ },
+ [types.SET_LINKS](state, links) {
+ Object.assign(state, { links });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
index f17ec4da308..e09f88878f4 100644
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -14,10 +14,6 @@ export default {
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
- commit: {
- ...branch.commit,
- pipeline: {},
- },
},
},
});
@@ -32,9 +28,4 @@ export default {
commit,
});
},
- [types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) {
- Object.assign(state.projects[projectId].branches[branchId].commit, {
- pipeline,
- });
- },
};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index e7411f16a4f..4aac4696075 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -23,4 +23,6 @@ export default () => ({
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
+ rightPane: null,
+ links: {},
});
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 741894b5e6c..cdb75752b4e 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -101,13 +101,19 @@ export default class IntegrationSettingsForm {
return axios.put(this.testEndPoint, formData)
.then(({ data }) => {
if (data.error) {
- flash(`${data.message} ${data.service_response}`, 'alert', document, {
- title: 'Save anyway',
- clickHandler: (e) => {
- e.preventDefault();
- this.$form.submit();
- },
- });
+ let flashActions;
+
+ if (data.test_failed) {
+ flashActions = {
+ title: 'Save anyway',
+ clickHandler: (e) => {
+ e.preventDefault();
+ this.$form.submit();
+ },
+ };
+ }
+
+ flash(`${data.message} ${data.service_response}`, 'alert', document, flashActions);
} else {
this.$form.submit();
}
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 90d4e19e90b..bb8b3d91e40 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -30,7 +30,7 @@ export default class IssuableForm {
}
this.initAutosave();
- this.form.on('submit:success', this.handleSubmit);
+ this.form.on('submit', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index c67bd7fb0c6..611e8200b4d 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -84,7 +84,7 @@ export default class Job {
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
then we use a polyfill
- **/
+ */
if (this.$topBar.css('position') !== 'static') return;
StickyFill.add(this.$topBar);
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 82fec7e936b..8f3c66b0cbe 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -48,11 +48,10 @@ export default {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
- let className = 'js-retry-button pull-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
+ let className =
+ 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className +=
- this.job.status && this.job.recoverable
- ? ' btn-primary'
- : ' btn-inverted-secondary';
+ this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
@@ -104,8 +103,7 @@ export default {
<button
type="button"
:aria-label="__('Toggle Sidebar')"
- class="btn btn-blank gutter-toggle pull-right
- d-block d-sm-block d-md-none js-sidebar-build-toggle"
+ class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js
index 5a216f8fae2..89019da9d1e 100644
--- a/app/assets/javascripts/jobs/job_details_mediator.js
+++ b/app/assets/javascripts/jobs/job_details_mediator.js
@@ -1,5 +1,3 @@
-/* global Build */
-
import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
@@ -50,7 +48,8 @@ export default class JobMediator {
}
getJob() {
- return this.service.getJob()
+ return this.service
+ .getJob()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 06023e98ec3..8b01024b7d4 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
import $ from 'jquery';
-import Sortable from 'vendor/Sortable';
+import Sortable from 'sortablejs';
import flash from './flash';
import axios from './lib/utils/axios_utils';
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index eafdaf4a672..7d0ff53f366 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -426,7 +426,7 @@ export default class LabelsSelect {
const tpl = _.template([
'<% _.each(labels, function(label){ %>',
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
- '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'<%- label.title %>',
'</span>',
'</a>',
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 2f4328b56e1..2cc5fb10027 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -9,7 +9,7 @@ delete window.translations;
Translates `text`
@param text The text to be translated
@returns {String} The translated text
-**/
+*/
const gettext = locale.gettext.bind(locale);
/**
@@ -21,7 +21,7 @@ const gettext = locale.gettext.bind(locale);
@param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days')
-**/
+*/
const ngettext = (text, pluralText, count) => {
const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
@@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => {
(eg. 'Context')
@param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text
-**/
+*/
const pgettext = (keyOrContext, key) => {
const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
const translated = gettext(normalizedKey).split('|');
diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js
index 5f4a053f98e..599104dcfa0 100644
--- a/app/assets/javascripts/locale/sprintf.js
+++ b/app/assets/javascripts/locale/sprintf.js
@@ -10,7 +10,7 @@ import _ from 'underscore';
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
@see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
-**/
+*/
export default (input, parameters, escapeParameters = true) => {
let output = input;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 3548c07aea8..493c119dc6f 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -362,7 +362,7 @@ export default class MergeRequestTabs {
//
// status - Boolean, true to show, false to hide
toggleLoading(status) {
- $('.mr-loading-status .loading').toggleClass('hidden', status);
+ $('.mr-loading-status .loading').toggleClass('hidden', !status);
}
diffViewType() {
@@ -427,7 +427,7 @@ export default class MergeRequestTabs {
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
then we default back to Bootstraps affix
- **/
+ */
if ($tabs.css('position') !== 'static') return;
const $diffTabs = $('#diff-notes-app');
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index c4de4826eda..5b5b1e89058 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -14,6 +14,7 @@ export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
+export const DESCRIPTION_TYPE = 'changed the description';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 98ce070288e..b2222476924 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -12,20 +12,13 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
-export const setNotesData = ({ commit }, data) =>
- commit(types.SET_NOTES_DATA, data);
-export const setNoteableData = ({ commit }, data) =>
- commit(types.SET_NOTEABLE_DATA, data);
-export const setUserData = ({ commit }, data) =>
- commit(types.SET_USER_DATA, data);
-export const setLastFetchedAt = ({ commit }, data) =>
- commit(types.SET_LAST_FETCHED_AT, data);
-export const setInitialNotes = ({ commit }, data) =>
- commit(types.SET_INITIAL_NOTES, data);
-export const setTargetNoteHash = ({ commit }, data) =>
- commit(types.SET_TARGET_NOTE_HASH, data);
-export const toggleDiscussion = ({ commit }, data) =>
- commit(types.TOGGLE_DISCUSSION, data);
+export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
+export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
+export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
+export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
+export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
+export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
+export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
export const fetchNotes = ({ commit }, path) =>
service
@@ -69,20 +62,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) =>
return res;
});
-export const removePlaceholderNotes = ({ commit }) =>
- commit(types.REMOVE_PLACEHOLDER_NOTES);
+export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
-export const toggleResolveNote = (
- { commit },
- { endpoint, isResolved, discussion },
-) =>
+export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) =>
service
.toggleResolveNote(endpoint, isResolved)
.then(res => res.json())
.then(res => {
- const mutationType = discussion
- ? types.UPDATE_DISCUSSION
- : types.UPDATE_NOTE;
+ const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
commit(mutationType, res);
});
@@ -114,7 +101,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
export const toggleStateButtonLoading = ({ commit }, value) =>
commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
-export const emitStateChangedEvent = ({ commit, getters }, data) => {
+export const emitStateChangedEvent = ({ getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', {
detail: {
data,
@@ -179,10 +166,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
loadAwardsHandler()
.then(awardsHandler => {
- awardsHandler.addAwardToEmojiBar(
- votesBlock,
- commandsChanges.emoji_award,
- );
+ awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
awardsHandler.scrollToAwards();
})
.catch(() => {
@@ -194,10 +178,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
}
- if (
- commandsChanges.spend_time != null ||
- commandsChanges.time_estimate != null
- ) {
+ if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
}
}
@@ -218,14 +199,8 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
resp.notes.forEach(note => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
- } else if (
- note.type === constants.DISCUSSION_NOTE ||
- note.type === constants.DIFF_NOTE
- ) {
- const discussion = utils.findNoteObjectById(
- state.notes,
- note.discussion_id,
- );
+ } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
+ const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
@@ -249,11 +224,8 @@ export const poll = ({ commit, state, getters }) => {
method: 'poll',
data: state,
successCallback: resp =>
- resp
- .json()
- .then(data => pollSuccessCallBack(data, commit, state, getters)),
- errorCallback: () =>
- Flash('Something went wrong while fetching latest comments.'),
+ resp.json().then(data => pollSuccessCallBack(data, commit, state, getters)),
+ errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
});
if (!Visibility.hidden()) {
@@ -292,14 +264,11 @@ export const fetchData = ({ commit, state, getters }) => {
.catch(() => Flash('Something went wrong while fetching latest comments.'));
};
-export const toggleAward = (
- { commit, state, getters, dispatch },
- { awardName, noteId },
-) => {
+export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
};
-export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => {
+export const toggleAwardRequest = ({ dispatch }, data) => {
const { endpoint, awardName } = data;
return service
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
new file mode 100644
index 00000000000..fa4a1c56b20
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -0,0 +1,108 @@
+import { n__, s__, sprintf } from '~/locale';
+import { DESCRIPTION_TYPE } from '../constants';
+
+/**
+ * Changes the description from a note, returns 'changed the description n number of times'
+ */
+export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => {
+ const descriptionNote = Object.assign({}, note);
+
+ descriptionNote.note_html = sprintf(
+ s__(`MergeRequest|
+ %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`),
+ {
+ paragraphStart: '<p dir="auto">',
+ paragraphEnd: '</p>',
+ descriptionChangedTimes,
+ timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes),
+ },
+ false,
+ );
+
+ descriptionNote.times_updated = descriptionChangedTimes;
+
+ return descriptionNote;
+};
+
+/**
+ * Checks the time difference between two notes from their 'created_at' dates
+ * returns an integer
+ */
+
+export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
+ const descriptionNoteBegin = new Date(noteBeggining.created_at);
+ const descriptionNoteEnd = new Date(noteEnd.created_at);
+ const timeDifferenceMinutes = (descriptionNoteEnd - descriptionNoteBegin) / 1000 / 60;
+
+ return Math.ceil(timeDifferenceMinutes);
+};
+
+/**
+ * Checks if a note is a system note and if the content is description
+ *
+ * @param {Object} note
+ * @returns {Boolean}
+ */
+export const isDescriptionSystemNote = note => note.system && note.note === DESCRIPTION_TYPE;
+
+/**
+ * Collapses the system notes of a description type, e.g. Changed the description, n minutes ago
+ * the notes will collapse as long as they happen no more than 10 minutes away from each away
+ * in between the notes can be anything, another type of system note
+ * (such as 'changed the weight') or a comment.
+ *
+ * @param {Array} notes
+ * @returns {Array}
+ */
+export const collapseSystemNotes = notes => {
+ let lastDescriptionSystemNote = null;
+ let lastDescriptionSystemNoteIndex = -1;
+ let descriptionChangedTimes = 1;
+
+ return notes.slice(0).reduce((acc, currentNote) => {
+ const note = currentNote.notes[0];
+
+ if (isDescriptionSystemNote(note)) {
+ // is it the first one?
+ if (!lastDescriptionSystemNote) {
+ lastDescriptionSystemNote = note;
+ lastDescriptionSystemNoteIndex = acc.length;
+ } else if (lastDescriptionSystemNote) {
+ const timeDifferenceMinutes = getTimeDifferenceMinutes(
+ lastDescriptionSystemNote,
+ note,
+ );
+
+ // are they less than 10 minutes appart?
+ if (timeDifferenceMinutes > 10) {
+ // reset counter
+ descriptionChangedTimes = 1;
+ // update the previous system note
+ lastDescriptionSystemNote = note;
+ lastDescriptionSystemNoteIndex = acc.length;
+ } else {
+ // increase counter
+ descriptionChangedTimes += 1;
+
+ // delete the previous one
+ acc.splice(lastDescriptionSystemNoteIndex, 1);
+
+ // replace the text of the current system note with the collapsed note.
+ currentNote.notes.splice(
+ 0,
+ 1,
+ changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes),
+ );
+
+ // update the previous system note index
+ lastDescriptionSystemNoteIndex = acc.length;
+ }
+ }
+ }
+ acc.push(currentNote);
+ return acc;
+ }, []);
+};
+
+// for babel-rewire
+export default {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 787be6f4c99..bc373e0d0fc 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,6 +1,8 @@
import _ from 'underscore';
+import { collapseSystemNotes } from './collapse_utils';
+
+export const notes = state => collapseSystemNotes(state.notes);
-export const notes = state => state.notes;
export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index bb91ac84ffb..8737f537296 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -1,9 +1,15 @@
import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
});
+
+document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
new file mode 100644
index 00000000000..d4f34e32a48
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
@@ -0,0 +1,5 @@
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initGkeDropdowns();
+});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 755a34b7348..06b0ab184ed 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -213,7 +213,7 @@
</i>
</div>
</div>
- <span class="help-block">{{ visibilityLevelDescription }}</span>
+ <span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
<label
v-if="visibilityLevel !== visibilityOptions.PRIVATE"
class="request-access"
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
index df21e2f8771..5765eed4d45 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -59,7 +59,7 @@ export default {
ref="form"
:action="deleteWikiUrl"
method="post"
- class="form-horizontal js-requires-input"
+ class="js-requires-input"
>
<input
ref="method"
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index 34a12ef76a1..dcd0b9a76ce 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,5 +1,7 @@
import bp from '../../../breakpoints';
import { slugify } from '../../../lib/utils/text_utility';
+import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
+import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
export default class Wikis {
constructor() {
@@ -28,7 +30,12 @@ export default class Wikis {
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
- window.location.href = `${wikisPath}/${slug}`;
+
+ // If the wiki is empty, we need to merge the current URL params to keep the "create" view.
+ const params = parseQueryStringIntoObject(window.location.search.substr(1));
+ const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
+ redirectTo(url);
+
e.preventDefault();
}
}
diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
index 53030045292..18c7b21cf8c 100644
--- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
+++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
@@ -5,7 +5,7 @@ import $ from 'jquery';
*
* Toggling this checkbox adds/removes a `remember_me` parameter to the
* login buttons' href, which is passed on to the omniauth callback.
- **/
+ */
export default class OAuthRememberMe {
constructor(opts = {}) {
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index ca375007ec5..9404b06615e 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -77,10 +77,9 @@ export default class UserTabs {
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this.windowLocation = window.location;
- this.$parentEl.find('.nav-links a')
- .each((i, navLink) => {
- this.loaded[$(navLink).attr('data-action')] = false;
- });
+ this.$parentEl.find('.nav-links a').each((i, navLink) => {
+ this.loaded[$(navLink).attr('data-action')] = false;
+ });
this.actions = Object.keys(this.loaded);
this.bindEvents();
@@ -116,8 +115,7 @@ export default class UserTabs {
}
activateTab(action) {
- return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
- .tab('show');
+ return this.$parentEl.find(`.nav-links .js-${action}-tab a`).tab('show');
}
setTab(action, endpoint) {
@@ -137,7 +135,8 @@ export default class UserTabs {
loadTab(action, endpoint) {
this.toggleLoading(true);
- return axios.get(endpoint)
+ return axios
+ .get(endpoint)
.then(({ data }) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
@@ -161,10 +160,11 @@ export default class UserTabs {
const utcOffset = $calendarWrap.data('utcOffset');
let utcFormatted = 'UTC';
if (utcOffset !== 0) {
- utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`;
+ utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${utcOffset / 3600}`;
}
- axios.get(calendarPath)
+ axios
+ .get(calendarPath)
.then(({ data }) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
@@ -180,17 +180,20 @@ export default class UserTabs {
}
toggleLoading(status) {
- return this.$parentEl.find('.loading-status .loading')
- .toggleClass('hidden', status);
+ return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status);
}
setCurrentAction(source) {
let newState = source;
newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash;
- history.replaceState({
- url: newState,
- }, document.title, newState);
+ history.replaceState(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
return newState;
}
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 7bfe11ab8cd..e64afc94ef9 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -79,12 +79,13 @@ export default {
};
</script>
<template>
- <div class="ci-job-dropdown-container dropdown">
+ <div class="ci-job-dropdown-container dropdown dropright">
<button
v-tooltip
type="button"
data-toggle="dropdown"
data-container="body"
+ data-boundary="viewport"
class="dropdown-menu-toggle build-content"
:title="tooltipText"
>
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index a7a2a7235fd..b37febe523c 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -99,7 +99,7 @@ Please update your Git repository remotes as soon as possible.`),
:disabled="isRequestPending"
/>
</div>
- <p class="help-block">
+ <p class="form-text text-muted">
{{ path }}
</p>
</div>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
new file mode 100644
index 00000000000..c15d8ba49e1
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
@@ -0,0 +1,71 @@
+import _ from 'underscore';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
+import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+import store from '../store';
+
+export default {
+ store,
+ components: {
+ LoadingIcon,
+ DropdownButton,
+ DropdownSearchInput,
+ DropdownHiddenInput,
+ },
+ props: {
+ fieldId: {
+ type: String,
+ required: true,
+ },
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ defaultValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ hasErrors: false,
+ searchQuery: '',
+ gapiError: '',
+ };
+ },
+ computed: {
+ results() {
+ if (!this.items) {
+ return [];
+ }
+
+ return this.items.filter(item => item.name.toLowerCase().indexOf(this.searchQuery) > -1);
+ },
+ },
+ methods: {
+ fetchSuccessHandler() {
+ if (this.defaultValue) {
+ const itemToSelect = _.find(this.items, item => item.name === this.defaultValue);
+
+ if (itemToSelect) {
+ this.setItem(itemToSelect.name);
+ }
+ }
+
+ this.isLoading = false;
+ this.hasErrors = false;
+ },
+ fetchFailureHandler(resp) {
+ this.isLoading = false;
+ this.hasErrors = true;
+
+ if (resp.result && resp.result.error) {
+ this.gapiError = resp.result.error.message;
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
new file mode 100644
index 00000000000..ab7d2d41ece
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
@@ -0,0 +1,142 @@
+<script>
+import { sprintf, s__ } from '~/locale';
+import { mapState, mapGetters, mapActions } from 'vuex';
+
+import gkeDropdownMixin from './gke_dropdown_mixin';
+
+export default {
+ name: 'GkeMachineTypeDropdown',
+ mixins: [gkeDropdownMixin],
+ computed: {
+ ...mapState([
+ 'isValidatingProjectBilling',
+ 'projectHasBillingEnabled',
+ 'selectedZone',
+ 'selectedMachineType',
+ ]),
+ ...mapState({ items: 'machineTypes' }),
+ ...mapGetters(['hasZone', 'hasMachineType']),
+ allDropdownsSelected() {
+ return this.projectHasBillingEnabled && this.hasZone && this.hasMachineType;
+ },
+ isDisabled() {
+ return (
+ this.isLoading ||
+ this.isValidatingProjectBilling ||
+ !this.projectHasBillingEnabled ||
+ !this.hasZone
+ );
+ },
+ toggleText() {
+ if (this.isLoading) {
+ return s__('ClusterIntegration|Fetching machine types');
+ }
+
+ if (this.selectedMachineType) {
+ return this.selectedMachineType;
+ }
+
+ if (!this.projectHasBillingEnabled && !this.hasZone) {
+ return s__('ClusterIntegration|Select project and zone to choose machine type');
+ }
+
+ return !this.hasZone
+ ? s__('ClusterIntegration|Select zone to choose machine type')
+ : s__('ClusterIntegration|Select machine type');
+ },
+ errorMessage() {
+ return sprintf(
+ s__(
+ 'ClusterIntegration|An error occured while trying to fetch zone machine types: %{error}',
+ ),
+ { error: this.gapiError },
+ );
+ },
+ },
+ watch: {
+ selectedZone() {
+ this.hasErrors = false;
+
+ if (this.hasZone) {
+ this.isLoading = true;
+
+ this.fetchMachineTypes()
+ .then(this.fetchSuccessHandler)
+ .catch(this.fetchFailureHandler);
+ }
+ },
+ selectedMachineType() {
+ this.enableSubmit();
+ },
+ },
+ methods: {
+ ...mapActions(['fetchMachineTypes']),
+ ...mapActions({ setItem: 'setMachineType' }),
+ enableSubmit() {
+ if (this.allDropdownsSelected) {
+ const submitButtonEl = document.querySelector('.js-gke-cluster-creation-submit');
+
+ if (submitButtonEl) {
+ submitButtonEl.removeAttribute('disabled');
+ }
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-gcp-machine-type-dropdown dropdown"
+ :class="{ 'gl-show-field-errors': hasErrors }"
+ >
+ <dropdown-hidden-input
+ :name="fieldName"
+ :value="selectedMachineType"
+ />
+ <dropdown-button
+ :class="{ 'gl-field-error-outline': hasErrors }"
+ :is-disabled="isDisabled"
+ :is-loading="isLoading"
+ :toggle-text="toggleText"
+ />
+ <div class="dropdown-menu dropdown-select">
+ <dropdown-search-input
+ v-model="searchQuery"
+ :placeholder-text="s__('ClusterIntegration|Search machine types')"
+ />
+ <div class="dropdown-content">
+ <ul>
+ <li v-show="!results.length">
+ <span class="menu-item">
+ {{ s__('ClusterIntegration|No machine types matched your search') }}
+ </span>
+ </li>
+ <li
+ v-for="result in results"
+ :key="result.id"
+ >
+ <button
+ type="button"
+ @click.prevent="setItem(result.name)"
+ >
+ {{ result.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ </div>
+ </div>
+ <span
+ class="form-text text-muted"
+ :class="{ 'gl-field-error': hasErrors }"
+ v-if="hasErrors"
+ >
+ {{ errorMessage }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
new file mode 100644
index 00000000000..25350ef0fa9
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
@@ -0,0 +1,201 @@
+<script>
+import _ from 'underscore';
+import { s__, sprintf } from '~/locale';
+import { mapState, mapGetters, mapActions } from 'vuex';
+
+import gkeDropdownMixin from './gke_dropdown_mixin';
+
+export default {
+ name: 'GkeProjectIdDropdown',
+ mixins: [gkeDropdownMixin],
+ props: {
+ docsUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['selectedProject', 'isValidatingProjectBilling', 'projectHasBillingEnabled']),
+ ...mapState({ items: 'projects' }),
+ ...mapGetters(['hasProject']),
+ hasOneProject() {
+ return this.items && this.items.length === 1;
+ },
+ isDisabled() {
+ return (
+ this.isLoading || this.isValidatingProjectBilling || (this.items && this.items.length < 2)
+ );
+ },
+ toggleText() {
+ if (this.isValidatingProjectBilling) {
+ return s__('ClusterIntegration|Validating project billing status');
+ }
+
+ if (this.isLoading) {
+ return s__('ClusterIntegration|Fetching projects');
+ }
+
+ if (this.hasProject) {
+ return this.selectedProject.name;
+ }
+
+ if (!this.items) {
+ return s__('ClusterIntegration|No projects found');
+ }
+
+ return s__('ClusterIntegration|Select project');
+ },
+ helpText() {
+ let message;
+ if (this.hasErrors) {
+ return this.errorMessage;
+ }
+
+ if (!this.items) {
+ message =
+ 'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
+ }
+
+ message =
+ this.items && this.items.length
+ ? 'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.'
+ : 'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
+
+ return sprintf(
+ s__(message),
+ {
+ docsLinkEnd: '&nbsp;<i class="fa fa-external-link" aria-hidden="true"></i></a>',
+ docsLinkStart: `<a href="${_.escape(
+ this.docsUrl,
+ )}" target="_blank" rel="noopener noreferrer">`,
+ },
+ false,
+ );
+ },
+ errorMessage() {
+ if (!this.projectHasBillingEnabled) {
+ if (this.gapiError) {
+ return s__(
+ 'ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again.',
+ );
+ }
+
+ return sprintf(
+ s__(
+ 'This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target="_blank" rel="noopener noreferrer">enable billing <i class="fa fa-external-link" aria-hidden="true"></i></a> and try again.',
+ ),
+ {
+ linkToBilling:
+ 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral',
+ },
+ false,
+ );
+ }
+
+ return sprintf(
+ s__('ClusterIntegration|An error occured while trying to fetch your projects: %{error}'),
+ { error: this.gapiError },
+ );
+ },
+ },
+ watch: {
+ selectedProject() {
+ this.setIsValidatingProjectBilling(true);
+
+ this.validateProjectBilling()
+ .then(this.validateProjectBillingSuccessHandler)
+ .catch(this.validateProjectBillingFailureHandler);
+ },
+ },
+ created() {
+ this.isLoading = true;
+
+ this.fetchProjects()
+ .then(this.fetchSuccessHandler)
+ .catch(this.fetchFailureHandler);
+ },
+ methods: {
+ ...mapActions(['fetchProjects', 'setIsValidatingProjectBilling', 'validateProjectBilling']),
+ ...mapActions({ setItem: 'setProject' }),
+ fetchSuccessHandler() {
+ if (this.defaultValue) {
+ const projectToSelect = _.find(this.items, item => item.projectId === this.defaultValue);
+
+ if (projectToSelect) {
+ this.setItem(projectToSelect);
+ }
+ } else if (this.items.length === 1) {
+ this.setItem(this.items[0]);
+ }
+
+ this.isLoading = false;
+ this.hasErrors = false;
+ },
+ validateProjectBillingSuccessHandler() {
+ this.hasErrors = !this.projectHasBillingEnabled;
+ },
+ validateProjectBillingFailureHandler(resp) {
+ this.hasErrors = true;
+
+ this.gapiError = resp.result ? resp.result.error.message : resp;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-gcp-project-id-dropdown dropdown"
+ :class="{ 'gl-show-field-errors': hasErrors }"
+ >
+ <dropdown-hidden-input
+ :name="fieldName"
+ :value="selectedProject.projectId"
+ />
+ <dropdown-button
+ :class="{
+ 'gl-field-error-outline': hasErrors,
+ 'read-only': hasOneProject
+ }"
+ :is-disabled="isDisabled"
+ :is-loading="isLoading"
+ :toggle-text="toggleText"
+ />
+ <div class="dropdown-menu dropdown-select">
+ <dropdown-search-input
+ v-model="searchQuery"
+ :placeholder-text="s__('ClusterIntegration|Search projects')"
+ />
+ <div class="dropdown-content">
+ <ul>
+ <li v-show="!results.length">
+ <span class="menu-item">
+ {{ s__('ClusterIntegration|No projects matched your search') }}
+ </span>
+ </li>
+ <li
+ v-for="result in results"
+ :key="result.project_number"
+ >
+ <button
+ type="button"
+ @click.prevent="setItem(result)"
+ >
+ {{ result.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ </div>
+ </div>
+ <span
+ class="form-text text-muted"
+ :class="{ 'gl-field-error': hasErrors }"
+ v-html="helpText"
+ ></span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
new file mode 100644
index 00000000000..8ee4eefcd91
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
@@ -0,0 +1,116 @@
+<script>
+import { sprintf, s__ } from '~/locale';
+import { mapState, mapActions } from 'vuex';
+
+import gkeDropdownMixin from './gke_dropdown_mixin';
+
+export default {
+ name: 'GkeZoneDropdown',
+ mixins: [gkeDropdownMixin],
+ computed: {
+ ...mapState([
+ 'selectedProject',
+ 'selectedZone',
+ 'projects',
+ 'isValidatingProjectBilling',
+ 'projectHasBillingEnabled',
+ ]),
+ ...mapState({ items: 'zones' }),
+ isDisabled() {
+ return this.isLoading || this.isValidatingProjectBilling || !this.projectHasBillingEnabled;
+ },
+ toggleText() {
+ if (this.isLoading) {
+ return s__('ClusterIntegration|Fetching zones');
+ }
+
+ if (this.selectedZone) {
+ return this.selectedZone;
+ }
+
+ return !this.projectHasBillingEnabled
+ ? s__('ClusterIntegration|Select project to choose zone')
+ : s__('ClusterIntegration|Select zone');
+ },
+ errorMessage() {
+ return sprintf(
+ s__('ClusterIntegration|An error occured while trying to fetch project zones: %{error}'),
+ { error: this.gapiError },
+ );
+ },
+ },
+ watch: {
+ isValidatingProjectBilling(isValidating) {
+ this.hasErrors = false;
+
+ if (!isValidating && this.projectHasBillingEnabled) {
+ this.isLoading = true;
+
+ this.fetchZones()
+ .then(this.fetchSuccessHandler)
+ .catch(this.fetchFailureHandler);
+ }
+ },
+ },
+ methods: {
+ ...mapActions(['fetchZones']),
+ ...mapActions({ setItem: 'setZone' }),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-gcp-zone-dropdown dropdown"
+ :class="{ 'gl-show-field-errors': hasErrors }"
+ >
+ <dropdown-hidden-input
+ :name="fieldName"
+ :value="selectedZone"
+ />
+ <dropdown-button
+ :class="{ 'gl-field-error-outline': hasErrors }"
+ :is-disabled="isDisabled"
+ :is-loading="isLoading"
+ :toggle-text="toggleText"
+ />
+ <div class="dropdown-menu dropdown-select">
+ <dropdown-search-input
+ v-model="searchQuery"
+ :placeholder-text="s__('ClusterIntegration|Search zones')"
+ />
+ <div class="dropdown-content">
+ <ul>
+ <li v-show="!results.length">
+ <span class="menu-item">
+ {{ s__('ClusterIntegration|No zones matched your search') }}
+ </span>
+ </li>
+ <li
+ v-for="result in results"
+ :key="result.id"
+ >
+ <button
+ type="button"
+ @click.prevent="setItem(result.name)"
+ >
+ {{ result.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ </div>
+ </div>
+ <span
+ class="form-text text-muted"
+ :class="{ 'gl-field-error': hasErrors }"
+ v-if="hasErrors"
+ >
+ {{ errorMessage }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js
new file mode 100644
index 00000000000..2a1c0819916
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js
@@ -0,0 +1,11 @@
+import { s__ } from '~/locale';
+
+export const GCP_API_ERROR = s__(
+ 'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.',
+);
+export const GCP_API_CLOUD_BILLING_ENDPOINT =
+ 'https://www.googleapis.com/discovery/v1/apis/cloudbilling/v1/rest';
+export const GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT =
+ 'https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest';
+export const GCP_API_COMPUTE_ENDPOINT =
+ 'https://www.googleapis.com/discovery/v1/apis/compute/v1/rest';
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js
new file mode 100644
index 00000000000..729b9404b64
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js
@@ -0,0 +1,88 @@
+/* global gapi */
+import Vue from 'vue';
+import Flash from '~/flash';
+import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
+import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
+import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
+import * as CONSTANTS from './constants';
+
+const mountComponent = (entryPoint, component, componentName, extraProps = {}) => {
+ const el = document.querySelector(entryPoint);
+ if (!el) return false;
+
+ const hiddenInput = el.querySelector('input');
+
+ return new Vue({
+ el,
+ components: {
+ [componentName]: component,
+ },
+ render: createElement =>
+ createElement(componentName, {
+ props: {
+ fieldName: hiddenInput.getAttribute('name'),
+ fieldId: hiddenInput.getAttribute('id'),
+ defaultValue: hiddenInput.value,
+ ...extraProps,
+ },
+ }),
+ });
+};
+
+const mountGkeProjectIdDropdown = () => {
+ const entryPoint = '.js-gcp-project-id-dropdown-entry-point';
+ const el = document.querySelector(entryPoint);
+
+ mountComponent(entryPoint, GkeProjectIdDropdown, 'gke-project-id-dropdown', {
+ docsUrl: el.dataset.docsurl,
+ });
+};
+
+const mountGkeZoneDropdown = () => {
+ mountComponent('.js-gcp-zone-dropdown-entry-point', GkeZoneDropdown, 'gke-zone-dropdown');
+};
+
+const mountGkeMachineTypeDropdown = () => {
+ mountComponent(
+ '.js-gcp-machine-type-dropdown-entry-point',
+ GkeMachineTypeDropdown,
+ 'gke-machine-type-dropdown',
+ );
+};
+
+const gkeDropdownErrorHandler = () => {
+ Flash(CONSTANTS.GCP_API_ERROR);
+};
+
+const initializeGapiClient = () => {
+ const el = document.querySelector('.js-gke-cluster-creation');
+ if (!el) return false;
+
+ return gapi.client
+ .init({
+ discoveryDocs: [
+ CONSTANTS.GCP_API_CLOUD_BILLING_ENDPOINT,
+ CONSTANTS.GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT,
+ CONSTANTS.GCP_API_COMPUTE_ENDPOINT,
+ ],
+ })
+ .then(() => {
+ gapi.client.setToken({ access_token: el.dataset.token });
+
+ mountGkeProjectIdDropdown();
+ mountGkeZoneDropdown();
+ mountGkeMachineTypeDropdown();
+ })
+ .catch(gkeDropdownErrorHandler);
+};
+
+const initGkeDropdowns = () => {
+ if (!gapi) {
+ gkeDropdownErrorHandler();
+ return false;
+ }
+
+ return gapi.load('client', initializeGapiClient);
+};
+
+export default initGkeDropdowns;
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
new file mode 100644
index 00000000000..4834a856271
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
@@ -0,0 +1,95 @@
+/* global gapi */
+import * as types from './mutation_types';
+
+const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
+ new Promise((resolve, reject) => {
+ const request = resource.list(params);
+
+ return request.then(
+ resp => {
+ const { result } = resp;
+
+ commit(mutation, result[payloadKey]);
+
+ resolve();
+ },
+ resp => {
+ reject(resp);
+ },
+ );
+ });
+
+export const setProject = ({ commit }, selectedProject) => {
+ commit(types.SET_PROJECT, selectedProject);
+};
+
+export const setZone = ({ commit }, selectedZone) => {
+ commit(types.SET_ZONE, selectedZone);
+};
+
+export const setMachineType = ({ commit }, selectedMachineType) => {
+ commit(types.SET_MACHINE_TYPE, selectedMachineType);
+};
+
+export const setIsValidatingProjectBilling = ({ commit }, isValidatingProjectBilling) => {
+ commit(types.SET_IS_VALIDATING_PROJECT_BILLING, isValidatingProjectBilling);
+};
+
+export const fetchProjects = ({ commit }) =>
+ gapiResourceListRequest({
+ resource: gapi.client.cloudresourcemanager.projects,
+ params: {},
+ commit,
+ mutation: types.SET_PROJECTS,
+ payloadKey: 'projects',
+ });
+
+export const validateProjectBilling = ({ dispatch, commit, state }) =>
+ new Promise((resolve, reject) => {
+ const request = gapi.client.cloudbilling.projects.getBillingInfo({
+ name: `projects/${state.selectedProject.projectId}`,
+ });
+
+ commit(types.SET_ZONE, '');
+ commit(types.SET_MACHINE_TYPE, '');
+
+ return request.then(
+ resp => {
+ const { billingEnabled } = resp.result;
+
+ commit(types.SET_PROJECT_BILLING_STATUS, !!billingEnabled);
+ dispatch('setIsValidatingProjectBilling', false);
+ resolve();
+ },
+ resp => {
+ dispatch('setIsValidatingProjectBilling', false);
+ reject(resp);
+ },
+ );
+ });
+
+export const fetchZones = ({ commit, state }) =>
+ gapiResourceListRequest({
+ resource: gapi.client.compute.zones,
+ params: {
+ project: state.selectedProject.projectId,
+ },
+ commit,
+ mutation: types.SET_ZONES,
+ payloadKey: 'items',
+ });
+
+export const fetchMachineTypes = ({ commit, state }) =>
+ gapiResourceListRequest({
+ resource: gapi.client.compute.machineTypes,
+ params: {
+ project: state.selectedProject.projectId,
+ zone: state.selectedZone,
+ },
+ commit,
+ mutation: types.SET_MACHINE_TYPES,
+ payloadKey: 'items',
+ });
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
new file mode 100644
index 00000000000..e39f02d0894
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
@@ -0,0 +1,3 @@
+export const hasProject = state => !!state.selectedProject.projectId;
+export const hasZone = state => !!state.selectedZone;
+export const hasMachineType = state => !!state.selectedMachineType;
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js
new file mode 100644
index 00000000000..5f72060633e
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import createState from './state';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: createState(),
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js
new file mode 100644
index 00000000000..45a91efc2d9
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js
@@ -0,0 +1,8 @@
+export const SET_PROJECT = 'SET_PROJECT';
+export const SET_PROJECT_BILLING_STATUS = 'SET_PROJECT_BILLING_STATUS';
+export const SET_IS_VALIDATING_PROJECT_BILLING = 'SET_IS_VALIDATING_PROJECT_BILLING';
+export const SET_ZONE = 'SET_ZONE';
+export const SET_MACHINE_TYPE = 'SET_MACHINE_TYPE';
+export const SET_PROJECTS = 'SET_PROJECTS';
+export const SET_ZONES = 'SET_ZONES';
+export const SET_MACHINE_TYPES = 'SET_MACHINE_TYPES';
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js
new file mode 100644
index 00000000000..88a2c1b630d
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js
@@ -0,0 +1,28 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_PROJECT](state, selectedProject) {
+ Object.assign(state, { selectedProject });
+ },
+ [types.SET_IS_VALIDATING_PROJECT_BILLING](state, isValidatingProjectBilling) {
+ Object.assign(state, { isValidatingProjectBilling });
+ },
+ [types.SET_PROJECT_BILLING_STATUS](state, projectHasBillingEnabled) {
+ Object.assign(state, { projectHasBillingEnabled });
+ },
+ [types.SET_ZONE](state, selectedZone) {
+ Object.assign(state, { selectedZone });
+ },
+ [types.SET_MACHINE_TYPE](state, selectedMachineType) {
+ Object.assign(state, { selectedMachineType });
+ },
+ [types.SET_PROJECTS](state, projects) {
+ Object.assign(state, { projects });
+ },
+ [types.SET_ZONES](state, zones) {
+ Object.assign(state, { zones });
+ },
+ [types.SET_MACHINE_TYPES](state, machineTypes) {
+ Object.assign(state, { machineTypes });
+ },
+};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js
new file mode 100644
index 00000000000..9f3c473d4bc
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js
@@ -0,0 +1,13 @@
+export default () => ({
+ selectedProject: {
+ projectId: '',
+ name: '',
+ },
+ selectedZone: '',
+ selectedMachineType: '',
+ isValidatingProjectBilling: null,
+ projectHasBillingEnabled: null,
+ projects: [],
+ zones: [],
+ machineTypes: [],
+});
diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js
index ae54fa5f1a9..658caeecde1 100644
--- a/app/assets/javascripts/raven/raven_config.js
+++ b/app/assets/javascripts/raven/raven_config.js
@@ -37,7 +37,7 @@ const IGNORE_URLS = [
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
- /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
+ /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
];
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 593a43c7cc1..c0de03373d8 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -7,9 +7,10 @@ Vue.use(VueResource);
export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING);
- return Vue.http.get(state.endpoint)
+ return Vue.http
+ .get(state.endpoint)
.then(res => res.json())
- .then((response) => {
+ .then(response => {
commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, response);
});
@@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => {
export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
- return Vue.http.get(repo.tagsPath, { params: { page } })
- .then((response) => {
- const headers = response.headers;
+ return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
+ const headers = response.headers;
- return response.json().then((resp) => {
- commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
- commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
- });
+ return response.json().then(resp => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+ commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
});
+ });
};
+// eslint-disable-next-line no-unused-vars
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
+// eslint-disable-next-line no-unused-vars
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 8c7e0664559..eb581b807a2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -168,8 +168,8 @@
<a
:href="mr.mergeCommitPath"
class="commit-sha js-mr-merged-commit-sha"
+ v-text="mr.shortMergeCommitSha"
>
- {{ mr.shortMergeCommitSha }}
</a>
<clipboard-button
:title="__('Copy commit SHA to clipboard')"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
index 926a3172412..875c3323dbb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
@@ -1,15 +1,63 @@
-/*
-The squash-before-merge button is EE only, but it's located right in the middle
-of the readyToMerge state component template.
-
-If we didn't declare this component in CE, we'd need to maintain a separate copy
-of the readyToMergeState template in EE, which is pretty big and likely to change.
-
-Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
-In EE, the configuration extends this object to add a functioning squash-before-merge
-button.
-*/
-
<script>
- export default {};
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ isMergeButtonDisabled: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ squashBeforeMerge: this.mr.squash,
+ };
+ },
+ methods: {
+ updateSquashModel() {
+ eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge);
+ },
+ },
+};
</script>
+
+<template>
+ <div class="accept-control inline">
+ <label class="merge-param-checkbox">
+ <input
+ type="checkbox"
+ name="squash"
+ class="qa-squash-checkbox"
+ :disabled="isMergeButtonDisabled"
+ v-model="squashBeforeMerge"
+ @change="updateSquashModel"
+ />
+ {{ __('Squash commits') }}
+ </label>
+ <a
+ :href="mr.squashBeforeMergeHelpPath"
+ data-title="About this feature"
+ data-placement="bottom"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ data-container="body"
+ v-tooltip
+ >
+ <icon
+ name="question-o"
+ />
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 1d1c8ebc179..3a194320bd8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -6,11 +6,13 @@ import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
+import SquashBeforeMerge from './mr_widget_squash_before_merge.vue';
export default {
name: 'ReadyToMerge',
components: {
statusIcon,
+ 'squash-before-merge': SquashBeforeMerge,
},
props: {
mr: { type: Object, required: true },
@@ -101,6 +103,12 @@ export default {
return enableSquashBeforeMerge && commitsCount > 1;
},
},
+ created() {
+ eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash);
+ },
+ beforeDestroy() {
+ eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash);
+ },
methods: {
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
@@ -128,13 +136,9 @@ export default {
commit_message: this.commitMessage,
merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
should_remove_source_branch: this.removeSourceBranch === true,
+ squash: this.mr.squash,
};
- // Only truthy in EE extension of this component
- if (this.setAdditionalParams) {
- this.setAdditionalParams(options);
- }
-
this.isMakingRequest = true;
this.service.merge(options)
.then(res => res.data)
@@ -154,6 +158,9 @@ export default {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
+ handleUpdateSquash(val) {
+ this.mr.squash = val;
+ },
initiateMergePolling() {
simplePoll((continuePolling, stopPolling) => {
this.handleMergePolling(continuePolling, stopPolling);
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 83b7b054e6f..e5b7e1f1c68 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -15,6 +15,11 @@ export default class MergeRequestStore {
const currentUser = data.current_user;
const pipelineStatus = data.pipeline ? data.pipeline.details.status : null;
+ this.squash = data.squash;
+ this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath ||
+ data.squash_before_merge_help_path;
+ this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
+
this.title = data.title;
this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch;
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index fcab8f571dd..03f924ba99d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -22,6 +22,8 @@ import Icon from '../../vue_shared/components/icon.vue';
* - Jobs show view header
* - Jobs show view sidebar
*/
+const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
+
export default {
components: {
Icon,
@@ -31,17 +33,36 @@ export default {
type: Object,
required: true,
},
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
+ },
+ },
+ borderless: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
+ icon() {
+ return this.borderless ? `${this.status.icon}_borderless` : this.status.icon;
+ },
},
};
</script>
<template>
<span :class="cssClass">
- <icon :name="status.icon" />
+ <icon
+ :name="icon"
+ :size="size"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
new file mode 100644
index 00000000000..c159333d89a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -0,0 +1,55 @@
+<script>
+import { __ } from '~/locale';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ },
+ props: {
+ isDisabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ toggleText: {
+ type: String,
+ required: false,
+ default: __('Select'),
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ class="dropdown-menu-toggle dropdown-menu-full-width"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false"
+ :disabled="isDisabled || isLoading"
+ >
+ <loading-icon
+ v-show="isLoading"
+ :inline="true"
+ />
+ <span class="dropdown-toggle-text">
+ {{ toggleText }}
+ </span>
+ <span
+ class="dropdown-toggle-icon"
+ v-show="!isLoading"
+ >
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true"
+ data-hidden="true"
+ ></i>
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
index 1832c3c1757..1fe27eb97ab 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
@@ -5,8 +5,8 @@ export default {
type: String,
required: true,
},
- label: {
- type: Object,
+ value: {
+ type: [Number, String],
required: true,
},
},
@@ -17,6 +17,6 @@ export default {
<input
type="hidden"
:name="name"
- :value="label.id"
+ :value="value"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
new file mode 100644
index 00000000000..c2145a26e64
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
@@ -0,0 +1,46 @@
+<script>
+import { __ } from '~/locale';
+
+export default {
+ props: {
+ placeholderText: {
+ type: String,
+ required: true,
+ default: __('Search'),
+ },
+ },
+ data() {
+ return { searchQuery: this.value };
+ },
+ watch: {
+ searchQuery(query) {
+ this.$emit('input', query);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-input">
+ <input
+ class="dropdown-input-field"
+ type="search"
+ v-model="searchQuery"
+ :placeholder="placeholderText"
+ autocomplete="off"
+ />
+ <i
+ class="fa fa-search dropdown-input-search"
+ aria-hidden="true"
+ data-hidden="true"
+ >
+ </i>
+ <i
+ class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
+ aria-hidden="true"
+ data-hidden="true"
+ role="button"
+ >
+ </i>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 70b46a9c2bb..f155ac2be02 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -2,13 +2,13 @@
import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
+import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import LoadingIcon from '../../loading_icon.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import DropdownButton from './dropdown_button.vue';
-import DropdownHiddenInput from './dropdown_hidden_input.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
@@ -140,7 +140,7 @@ export default {
v-for="label in context.labels"
:key="label.id"
:name="hiddenInputName"
- :label="label"
+ :value="label.id"
/>
<div
class="dropdown"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
index 69d588eb25d..88360b46f24 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -35,7 +35,12 @@ export default {
</script>
<template>
- <div class="hide-collapsed value issuable-show-labels js-value">
+ <div
+ class="hide-collapsed value issuable-show-labels js-value"
+ :class="{
+ 'has-labels':!isEmpty,
+ }"
+ >
<span
v-if="isEmpty"
class="text-secondary"
@@ -50,7 +55,7 @@ export default {
>
<span
v-tooltip
- class="label color-label"
+ class="badge color-label"
data-placement="bottom"
data-container="body"
:style="labelStyle(label)"
diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue
new file mode 100644
index 00000000000..2a35d6bc151
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue
@@ -0,0 +1,42 @@
+<script>
+export default {
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ active: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ // props can't be updated, so we map it to data where we can
+ localActive: this.active,
+ };
+ },
+ watch: {
+ active() {
+ this.localActive = this.active;
+ },
+ },
+ created() {
+ this.isTab = true;
+ },
+};
+</script>
+
+<template>
+ <div
+ class="tab-pane"
+ :class="{
+ active: localActive
+ }"
+ role="tabpanel"
+ >
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
new file mode 100644
index 00000000000..4362264caa5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
@@ -0,0 +1,64 @@
+export default {
+ data() {
+ return {
+ currentIndex: 0,
+ tabs: [],
+ };
+ },
+ mounted() {
+ this.updateTabs();
+ },
+ methods: {
+ updateTabs() {
+ this.tabs = this.$children.filter(child => child.isTab);
+ this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
+ },
+ setTab(index) {
+ this.tabs[this.currentIndex].localActive = false;
+ this.tabs[index].localActive = true;
+
+ this.currentIndex = index;
+ },
+ },
+ render(h) {
+ const navItems = this.tabs.map((tab, i) =>
+ h(
+ 'li',
+ {
+ key: i,
+ },
+ [
+ h(
+ 'a',
+ {
+ class: tab.localActive ? 'active' : null,
+ attrs: {
+ href: '#',
+ },
+ on: {
+ click: () => this.setTab(i),
+ },
+ },
+ tab.$slots.title || tab.title,
+ ),
+ ],
+ ),
+ );
+ const nav = h(
+ 'ul',
+ {
+ class: 'nav-links tab-links',
+ },
+ [navItems],
+ );
+ const content = h(
+ 'div',
+ {
+ class: ['tab-content'],
+ },
+ [this.$slots.default],
+ );
+
+ return h('div', {}, [[nav], content]);
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index 155e6b6698a..4f2412ce520 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -2,7 +2,9 @@ import $ from 'jquery';
export default {
bind(el) {
- $(el).tooltip();
+ $(el).tooltip({
+ trigger: 'hover',
+ });
},
componentUpdated(el) {
diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js
index 2c7886ec308..48c63373b77 100644
--- a/app/assets/javascripts/vue_shared/translate.js
+++ b/app/assets/javascripts/vue_shared/translate.js
@@ -13,7 +13,7 @@ export default (Vue) => {
@param text The text to be translated
@returns {String} The translated text
- **/
+ */
__,
/**
Translate the text with a number
@@ -24,7 +24,7 @@ export default (Vue) => {
@param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days')
- **/
+ */
n__,
/**
Translate context based text
@@ -36,7 +36,7 @@ export default (Vue) => {
(eg. 'Context')
@param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text
- **/
+ */
s__,
sprintf,
},
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 3b7ee5c73e6..d5679177f8f 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -36,6 +36,17 @@ html [type="button"],
cursor: pointer;
}
+input[type="file"] {
+ // Bootstrap 4 file input height is taller by default
+ // which makes them look ugly
+ line-height: 1;
+}
+
+b,
+strong {
+ font-weight: bold;
+}
+
a {
color: $gl-link-color;
}
@@ -48,6 +59,27 @@ a {
}
}
+code {
+ padding: 2px 4px;
+ color: $red-600;
+ background-color: $red-100;
+ border-radius: 3px;
+
+ .code & {
+ background-color: inherit;
+ padding: unset;
+ }
+
+ .build-trace & {
+ background-color: inherit;
+ padding: inherit;
+ }
+}
+
+.code {
+ padding: 9.5px;
+}
+
table {
// Remove any table border lines
border-spacing: 0;
@@ -87,7 +119,8 @@ table {
display: none;
}
-.dropdown-toggle::after {
+.dropdown-toggle::after,
+.dropright .dropdown-menu-toggle::after {
// Remove bootstrap's dropdown caret
display: none;
}
@@ -131,6 +164,10 @@ table {
}
.card {
+ .card-title {
+ margin-bottom: 0;
+ }
+
&.card-without-border {
@extend .border-0;
}
@@ -144,6 +181,16 @@ table {
}
}
-.nav-tabs .nav-link {
- border: 0;
+.nav-tabs {
+ .nav-link {
+ border: 0;
+ }
+
+ .nav-item {
+ margin-bottom: 0;
+ }
+}
+
+pre code {
+ white-space: pre-wrap;
}
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
new file mode 100644
index 00000000000..658e0ff638e
--- /dev/null
+++ b/app/assets/stylesheets/errors.scss
@@ -0,0 +1,120 @@
+/*
+ * This is a minimal stylesheet, meant to be used for error pages.
+ */
+@import 'framework/variables';
+@import '../../../node_modules/bootstrap/scss/functions';
+@import '../../../node_modules/bootstrap/scss/variables';
+@import '../../../node_modules/bootstrap/scss/mixins';
+@import '../../../node_modules/bootstrap/scss/reboot';
+@import '../../../node_modules/bootstrap/scss/buttons';
+@import '../../../node_modules/bootstrap/scss/forms';
+
+$body-color: #666;
+$header-color: #456;
+
+body {
+ color: $body-color;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: auto;
+ font-size: 14px;
+}
+
+h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: 400;
+ color: $header-color;
+}
+
+h2 {
+ font-size: 24px;
+ color: $body-color;
+ line-height: 1.5em;
+}
+
+h3 {
+ color: $header-color;
+ font-size: 20px;
+ font-weight: 400;
+ line-height: 28px;
+}
+
+img {
+ max-width: 80vw;
+ display: block;
+ margin: 40px auto;
+}
+
+a {
+ text-decoration: none;
+ color: $blue-600;
+}
+
+.page-container {
+ margin: auto 20px;
+}
+
+.container {
+ margin: auto;
+ max-width: 600px;
+ border-bottom: 1px solid $border-color;
+ padding-bottom: 1em;
+}
+
+.action-container {
+ padding: 0.5em 0;
+}
+
+.form-inline-flex {
+ display: flex;
+ flex-wrap: wrap;
+
+ button {
+ display: block;
+ width: 100%;
+ }
+
+ .field {
+ display: block;
+ width: 100%;
+ margin-bottom: 1em;
+ }
+
+ @include media-breakpoint-up(sm) {
+ flex-wrap: nowrap;
+
+ button {
+ width: auto;
+ }
+
+ .field {
+ margin-bottom: 0;
+ margin-right: 0.5em;
+ }
+ }
+}
+
+.error-nav {
+ padding: 0;
+ text-align: center;
+
+ li {
+ display: block;
+ padding-bottom: 1em;
+ }
+
+ @include media-breakpoint-up(sm) {
+
+ li {
+ display: inline-block;
+ padding-bottom: 0;
+
+ &:not(:first-child)::before {
+ content: '\00B7';
+ display: inline-block;
+ padding: 0 1em;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 1570b1f2eaa..74475daae14 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -35,6 +35,12 @@
@include media-breakpoint-down(xs) {
width: 100%;
}
+
+ &.projects-dropdown-menu {
+ padding: 0;
+ overflow-y: initial;
+ max-height: initial;
+ }
}
.dropdown-toggle,
@@ -63,6 +69,10 @@
border-radius: $border-radius-base;
white-space: nowrap;
+ &:disabled.read-only {
+ color: $gl-text-color !important;
+ }
+
&.no-outline {
outline: 0;
}
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index d6ae8cbb416..14dd3879bdc 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -35,7 +35,7 @@
}
&.active > a,
- &.dropdown.open > a {
+ &.dropdown.show > a {
color: $color-900;
background-color: $color-alternate;
}
@@ -74,7 +74,7 @@
}
&.active > a,
- &.dropdown.open > a {
+ &.dropdown.show > a {
color: $color-900;
background-color: $color-alternate;
@@ -169,11 +169,14 @@
color: $color-800;
}
- .nav-links li a.active {
- border-bottom: 2px solid $color-500;
+ .nav-links li {
+ &.active a,
+ a.active {
+ border-bottom: 2px solid $color-500;
- .badge.badge-pill {
- font-weight: $gl-font-weight-bold;
+ .badge.badge-pill {
+ font-weight: $gl-font-weight-bold;
+ }
}
}
@@ -189,6 +192,10 @@
&.active {
color: $color-700;
box-shadow: inset 3px 0 $color-700;
+
+ &.is-right {
+ box-shadow: inset -3px 0 $color-700;
+ }
}
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2085e5646ef..094134b63b0 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -297,12 +297,6 @@
display: flex;
margin: 0 0 0 6px;
- .projects-dropdown-menu {
- padding: 0;
- overflow-y: initial;
- max-height: initial;
- }
-
.dropdown-chevron {
position: relative;
top: -1px;
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 17f4958d535..d54490c87c6 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -2,7 +2,7 @@
* Well styled list
*
*/
-.card-body-list {
+.hover-list {
position: relative;
margin: 0;
padding: 0;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 0ea0b65b95f..d76cf8f8182 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -19,14 +19,23 @@
width: auto;
display: inline-block;
overflow-x: auto;
- border-left: 0;
- border-right: 0;
- border-bottom: 0;
+ border: 0;
+ border-color: $md-area-border;
@supports(width: fit-content) {
display: block;
width: fit-content;
}
+
+ tr {
+ th {
+ border-bottom: solid 2px $md-area-border;
+ }
+
+ td {
+ border-color: $md-area-border;
+ }
+ }
}
/*
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 667661d8b5c..a7896cc3fc3 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,6 +1,5 @@
.modal-header {
background-color: $modal-body-bg;
- padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title,
.modal-title {
@@ -74,12 +73,6 @@ body.modal-open {
}
}
-@include media-breakpoint-up(lg) {
- .modal-full {
- width: 98%;
- }
-}
-
.modal {
background-color: $black-transparent;
z-index: 2100;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index c3c64adf3da..847fc8c0792 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -31,14 +31,15 @@
color: $black;
}
}
+ }
- &.active {
- color: $black;
- font-weight: $gl-font-weight-bold;
+ &.active a,
+ a.active {
+ color: $black;
+ font-weight: $gl-font-weight-bold;
- .badge.badge-pill {
- color: $black;
- }
+ .badge.badge-pill {
+ color: $black;
}
}
}
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index f80e9b1014b..7152ef9bcfd 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -49,26 +49,11 @@
margin-top: 15px;
}
-.snippet-embed-input {
- height: 35px;
-}
-
.embed-snippet {
padding-right: 0;
padding-top: $gl-padding;
- .form-control {
- cursor: auto;
- width: 101%;
- margin-left: -1px;
- }
-
.embed-toggle-list li button {
padding: 8px 40px;
}
-
- .embed-toggle,
- .snippet-clipboard-btn {
- height: 35px;
- }
}
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
index 744fd0ff796..7cda674e5c8 100644
--- a/app/assets/stylesheets/framework/terms.scss
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -11,15 +11,15 @@
padding-top: $gl-padding;
}
- .panel {
- .panel-heading {
+ .card {
+ .card-header {
display: -webkit-flex;
display: flex;
align-items: center;
justify-content: space-between;
line-height: $line-height-base;
- .title {
+ .card-title {
display: flex;
align-items: center;
@@ -34,6 +34,8 @@
.navbar-collapse {
padding-right: 0;
+ flex-grow: 0;
+ flex-basis: auto;
.navbar-nav {
margin: 0;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index ed0bfbbe08b..9e77ea03a24 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -114,26 +114,27 @@
font-size: 0.95em;
}
+ blockquote,
.blockquote {
color: $gl-grayish-blue;
font-size: inherit;
padding: 8px 24px;
margin: 16px 0;
border-left: 3px solid $white-dark;
- }
- .blockquote:dir(rtl) {
- border-left: 0;
- border-right: 3px solid $white-dark;
- }
+ &:dir(rtl) {
+ border-left: 0;
+ border-right: 3px solid $white-dark;
+ }
- .blockquote p {
- color: $gl-grayish-blue !important;
- font-size: inherit;
- line-height: 1.5;
+ p {
+ color: $gl-grayish-blue !important;
+ font-size: inherit;
+ line-height: 1.5;
- &:last-child {
- margin: 0;
+ &:last-child {
+ margin: 0;
+ }
}
}
@@ -340,10 +341,6 @@ code {
}
}
-a > code {
- color: $link-color;
-}
-
.monospace {
font-family: $monospace_font;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 89b61530ddb..946223cfff0 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -373,6 +373,7 @@ $dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
$dropdown-item-hover-bg: $gray-darker;
$dropdown-fade-mask-height: 32px;
+$dropdown-member-form-control-width: 163px;
/*
* Filtered Search
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index b5eda79e5ed..1835c4364d3 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -138,6 +138,7 @@ pre {
margin: 0;
}
+blockquote,
.blockquote {
color: $gl-grayish-blue;
padding: 0 0 0 15px;
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 2b6d92016d5..56beb7718a4 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -6,13 +6,17 @@
.cluster-applications-table {
// Wait for the Vue to kick-in and render the applications block
- min-height: 400px;
+ min-height: 628px;
}
.clusters-dropdown-menu {
max-width: 100%;
}
+.clusters-error-alert {
+ width: 100%;
+}
+
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index a80061c511d..8ccf778e169 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -117,6 +117,10 @@
.prioritized-labels {
margin-bottom: 30px;
+ h5 {
+ font-size: $gl-font-size;
+ }
+
.add-priority {
display: none;
color: $gray-light;
@@ -131,6 +135,10 @@
}
.other-labels {
+ h5 {
+ font-size: $gl-font-size;
+ }
+
.remove-priority {
display: none;
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index de2b5701e2d..9914555d309 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -36,13 +36,12 @@
}
}
- .form-horizontal {
- margin-top: 20px;
+ .form-group {
+ margin-bottom: 0;
- @include media-breakpoint-up(sm) {
- display: -webkit-flex;
- display: flex;
- margin-top: 3px;
+ @include media-breakpoint-down(sm) {
+ display: block;
+ margin-left: 5px;
}
}
@@ -62,10 +61,15 @@
}
.member-form-control {
- @include media-breakpoint-down(xs) {
- padding-bottom: 5px;
+ @include media-breakpoint-down(sm) {
+ width: $dropdown-member-form-control-width;
margin-left: 0;
+ padding-bottom: 5px;
+ }
+
+ @include media-breakpoint-down(xs) {
margin-right: 0;
+ width: auto;
}
}
@@ -207,10 +211,6 @@
align-self: flex-start;
}
- .form-horizontal ~ .btn {
- margin-right: 0;
- }
-
@include media-breakpoint-down(xs) {
display: block;
@@ -220,6 +220,12 @@
display: block;
}
+ .controls > .btn:last-child {
+ margin-left: 5px;
+ margin-right: 5px;
+ width: auto;
+ }
+
.form-control {
width: 100%;
}
@@ -232,10 +238,6 @@
.member-controls {
margin-top: 5px;
}
-
- .form-horizontal {
- margin-top: 10px;
- }
}
}
@@ -259,10 +261,6 @@
margin-top: 0;
}
- .form-horizontal {
- display: block;
- }
-
.member-form-control {
margin: 5px 0;
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 06078f1d12e..5d0d59e12f2 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -405,7 +405,7 @@ table.u2f-registrations {
margin-right: $gl-padding / 4;
}
- .label-verification-status {
+ .badge-verification-status {
border-width: 1px;
border-style: solid;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 17d7087bd85..2b3cc33c8ae 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -183,7 +183,7 @@
svg {
position: relative;
- top: -1px;
+ top: -2px;
}
.ide-file-changed-icon {
@@ -458,6 +458,10 @@
width: auto;
margin-right: 0;
+ a {
+ height: 60px;
+ }
+
a:hover,
a:focus {
text-decoration: none;
@@ -546,7 +550,7 @@
margin-right: 0;
}
- &.help-block {
+ &.form-text.text-muted {
margin-left: 0;
right: 0;
}
@@ -718,9 +722,17 @@
}
.ide-new-btn {
+ .btn {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ }
+
+ .dropdown {
+ display: flex;
+ }
+
.dropdown-toggle svg {
- margin-top: -2px;
- margin-bottom: 2px;
+ top: 0;
}
.dropdown-menu {
@@ -877,6 +889,7 @@
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
outline: 0;
+ cursor: pointer;
svg {
margin: 0 auto;
@@ -909,6 +922,16 @@
width: 1px;
background: $white-light;
}
+
+ &.is-right {
+ padding-right: $gl-padding;
+ padding-left: $gl-padding + 1px;
+
+ &::after {
+ right: auto;
+ left: -1px;
+ }
+ }
}
}
@@ -952,7 +975,7 @@
height: 30px;
}
- .help-block {
+ .form-text.text-muted {
margin-top: 2px;
color: $blue-500;
cursor: pointer;
@@ -1088,10 +1111,6 @@
font-size: 12px;
}
-.ide-new-modal-label {
- line-height: 34px;
-}
-
.multi-file-commit-panel-success-message {
position: absolute;
top: 61px;
@@ -1125,3 +1144,112 @@
white-space: nowrap;
}
}
+
+.ide-external-link {
+ svg {
+ display: none;
+ }
+
+ &:hover,
+ &:focus {
+ svg {
+ display: inline-block;
+ }
+ }
+}
+
+.ide-right-sidebar {
+ width: auto;
+ min-width: 60px;
+
+ .ide-activity-bar {
+ border-left: 1px solid $white-dark;
+ }
+
+ .multi-file-commit-panel-inner {
+ width: 350px;
+ padding: $grid-size $gl-padding;
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
+ }
+}
+
+.ide-pipeline {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ .empty-state {
+ margin-top: auto;
+ margin-bottom: auto;
+
+ p {
+ margin: $grid-size 0;
+ text-align: center;
+ line-height: 24px;
+ }
+
+ .btn,
+ h4 {
+ margin: 0;
+ }
+ }
+}
+
+.ide-pipeline-list {
+ flex: 1;
+ overflow: auto;
+}
+
+.ide-pipeline-header {
+ min-height: 50px;
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+
+ .ci-status-icon {
+ display: flex;
+ }
+}
+
+.ide-job-item {
+ display: flex;
+ padding: 16px;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $border-color;
+ }
+
+ .ci-status-icon {
+ display: flex;
+ justify-content: center;
+ height: 20px;
+ margin-top: -2px;
+ overflow: hidden;
+ }
+}
+
+.ide-stage {
+ .card-header {
+ display: flex;
+ cursor: pointer;
+
+ .ci-status-icon {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ .card-body {
+ padding: 0;
+ }
+}
+
+.ide-stage-collapse-icon {
+ margin: auto 0 auto auto;
+}
+
+.ide-stage-title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 2b3773eebad..43c39c43bc1 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -174,7 +174,7 @@
.option-description,
.option-disabled-reason {
- margin-left: 45px;
+ margin-left: 30px;
color: $project-option-descr-color;
}
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 90ccd4abd90..bb10928a037 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -22,9 +22,9 @@
header,
nav,
-nav.main-nav,
nav.navbar-collapse,
nav.navbar-collapse.collapse,
+.nav-sidebar,
.profiler-results,
.tree-ref-holder,
.tree-holder .breadcrumb,
@@ -38,7 +38,8 @@ ul.notes-form,
.edit-link,
.note-action-button,
.right-sidebar,
-.flash-container {
+.flash-container,
+#js-peek {
display: none !important;
}