summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml5
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js32
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue114
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue84
-rw-r--r--app/assets/javascripts/clusters/constants.js13
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js141
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js45
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue2
-rw-r--r--app/assets/javascripts/import_projects/store/index.js2
-rw-r--r--app/assets/javascripts/main.js6
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js24
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/select2_select.vue2
-rw-r--r--app/assets/stylesheets/components/toast.scss3
-rw-r--r--app/assets/stylesheets/framework/header.scss41
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml3
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml8
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml4
-rw-r--r--app/views/search/_results.html.haml5
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml4
-rw-r--r--changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml5
-rw-r--r--changelogs/unreleased/46048-canary-next.yml5
-rw-r--r--changelogs/unreleased/57017-add-toast-success-message.yml5
-rw-r--r--changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml6
-rw-r--r--changelogs/unreleased/60874-fix-suggestion-misalignment.yml5
-rw-r--r--changelogs/unreleased/60906-fix-wiki-links.yml5
-rw-r--r--changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml5
-rw-r--r--changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml5
-rw-r--r--changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml5
-rw-r--r--config/initializers/sidekiq.rb17
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--locale/gitlab.pot12
-rw-r--r--package.json2
-rw-r--r--spec/controllers/search_controller_spec.rb24
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb2
-rw-r--r--spec/features/protected_branches_spec.rb26
-rw-r--r--spec/features/protected_tags_spec.rb13
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js47
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js73
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js134
-rw-r--r--spec/frontend/clusters/services/mock_data.js1
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js43
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js185
-rw-r--r--spec/frontend/import_projects/components/imported_project_table_row_spec.js (renamed from spec/javascripts/import_projects/components/imported_project_table_row_spec.js)25
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js (renamed from spec/javascripts/import_projects/components/provider_repo_table_row_spec.js)90
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js (renamed from spec/javascripts/import_projects/store/actions_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js (renamed from spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js172
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_warning_spec.js (renamed from spec/javascripts/vue_shared/components/issue/issue_warning_spec.js)10
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js (renamed from spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js)12
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/system_note_spec.js)0
-rw-r--r--spec/javascripts/import_projects/components/import_projects_table_spec.js188
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js234
-rw-r--r--spec/models/clusters/applications/runner_spec.rb4
-rw-r--r--spec/support/protected_branch_helpers.rb30
-rw-r--r--spec/support/protected_tag_helpers.rb18
-rw-r--r--yarn.lock14
66 files changed, 1200 insertions, 819 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index cc5d6060716..f5b131cf6b2 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -200,12 +200,11 @@ schedule:review-performance:
schedule:review-cleanup:
<<: *review-base
<<: *review-schedules-only
- stage: review
+ stage: build
allow_failure: true
- variables:
- GIT_DEPTH: "1"
environment:
name: review/auto-cleanup
+ action: stop
before_script:
- source scripts/utils.sh
- install_gitlab_gem
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index df855261b3c..4f47f1b6550 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,25 +1,20 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import {
- APPLICATION_STATUS,
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- UPGRADE_REQUESTED,
- UPGRADE_REQUEST_FAILURE,
- INGRESS,
- INGRESS_DOMAIN_SUFFIX,
-} from './constants';
+import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
+Vue.use(GlToast);
+
/**
* Cluster page has 2 separate parts:
* Toggle button and applications section
@@ -134,7 +129,6 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
- eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
@@ -144,7 +138,6 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
eventHub.$off('upgradeApplication', this.upgradeApplication);
- eventHub.$off('upgradeFailed', this.upgradeFailed);
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
@@ -258,12 +251,13 @@ export default class Clusters {
installApplication(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED);
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
+ this.store.installApplication(appId);
+
return this.service.installApplication(appId, data.params).catch(() => {
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
+ this.store.notifyInstallFailure(appId);
this.store.updateAppProperty(
appId,
'requestReason',
@@ -274,17 +268,15 @@ export default class Clusters {
upgradeApplication(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
- this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
- this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId));
- }
- upgradeFailed(appId) {
- this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE);
+ this.store.updateApplication(appId);
+ this.service.installApplication(appId, data.params).catch(() => {
+ this.store.notifyUpdateFailure(appId);
+ });
}
dismissUpgradeSuccess(appId) {
- this.store.updateAppProperty(appId, 'requestStatus', null);
+ this.store.acknowledgeSuccessfulUpdate(appId);
}
toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) {
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 937e4c3bfc3..a351916942e 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -8,12 +8,7 @@ import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue';
-import {
- APPLICATION_STATUS,
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- UPGRADE_REQUESTED,
-} from '../constants';
+import { APPLICATION_STATUS } from '../constants';
export default {
components: {
@@ -63,10 +58,6 @@ export default {
type: String,
required: false,
},
- requestStatus: {
- type: String,
- required: false,
- },
requestReason: {
type: String,
required: false,
@@ -76,6 +67,11 @@ export default {
required: false,
default: false,
},
+ installFailed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
version: {
type: String,
required: false,
@@ -88,6 +84,21 @@ export default {
type: Boolean,
required: false,
},
+ updateSuccessful: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateFailed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateAcknowledged: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
installApplicationRequestParams: {
type: Object,
required: false,
@@ -102,21 +113,12 @@ export default {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
isInstalling() {
- return (
- this.status === APPLICATION_STATUS.SCHEDULED ||
- this.status === APPLICATION_STATUS.INSTALLING ||
- (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.installed)
- );
+ return this.status === APPLICATION_STATUS.INSTALLING;
},
canInstall() {
- if (this.isInstalling) {
- return false;
- }
-
return (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
- this.status === APPLICATION_STATUS.ERROR ||
this.isUnknownStatus
);
},
@@ -137,7 +139,7 @@ export default {
return !this.installed || !this.uninstallable;
},
installButtonLoading() {
- return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling;
+ return !this.status || this.isInstalling;
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
@@ -168,19 +170,13 @@ export default {
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
- hasError() {
- return (
- !this.isInstalling &&
- (this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE)
- );
- },
generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
});
},
versionLabel() {
- if (this.upgradeFailed) {
+ if (this.updateFailed) {
return s__('ClusterIntegration|Upgrade failed');
} else if (this.isUpgrading) {
return s__('ClusterIntegration|Upgrading');
@@ -188,19 +184,6 @@ export default {
return s__('ClusterIntegration|Upgraded');
},
- upgradeRequested() {
- return this.requestStatus === UPGRADE_REQUESTED;
- },
- upgradeSuccessful() {
- return this.status === APPLICATION_STATUS.UPDATED;
- },
- upgradeFailed() {
- if (this.isUpgrading) {
- return false;
- }
-
- return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
- },
upgradeFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
},
@@ -211,11 +194,11 @@ export default {
},
upgradeButtonLabel() {
let label;
- if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
+ if (this.upgradeAvailable && !this.updateFailed && !this.isUpgrading) {
label = s__('ClusterIntegration|Upgrade');
} else if (this.isUpgrading) {
label = s__('ClusterIntegration|Updating');
- } else if (this.upgradeFailed) {
+ } else if (this.updateFailed) {
label = s__('ClusterIntegration|Retry update');
}
@@ -223,24 +206,19 @@ export default {
},
isUpgrading() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
- return (
- this.status === APPLICATION_STATUS.UPDATING ||
- (this.upgradeRequested && !this.upgradeSuccessful)
- );
+ return this.status === APPLICATION_STATUS.UPDATING;
},
shouldShowUpgradeDetails() {
// This method only returns true when;
// Upgrade was successful OR Upgrade failed
// AND new upgrade is unavailable AND version information is present.
- return (
- (this.upgradeSuccessful || this.upgradeFailed) && !this.upgradeAvailable && this.version
- );
+ return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
},
},
watch: {
- status() {
- if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) {
- eventHub.$emit('upgradeFailed', this.id);
+ updateSuccessful() {
+ if (this.updateSuccessful) {
+ this.$toast.show(this.upgradeSuccessDescription);
}
},
},
@@ -257,9 +235,6 @@ export default {
params: this.installApplicationRequestParams,
});
},
- dismissUpgradeSuccess() {
- eventHub.$emit('dismissUpgradeSuccess', this.id);
- },
},
};
</script>
@@ -297,7 +272,7 @@ export default {
</strong>
<slot name="description"></slot>
<div
- v-if="hasError || isUnknownStatus"
+ v-if="installFailed || isUnknownStatus"
class="cluster-application-error text-danger prepend-top-10"
>
<p class="js-cluster-application-general-error-message append-bottom-0">
@@ -318,10 +293,10 @@ export default {
class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
>
{{ versionLabel }}
- <span v-if="upgradeSuccessful">to</span>
+ <span v-if="updateSuccessful">to</span>
<gl-link
- v-if="upgradeSuccessful"
+ v-if="updateSuccessful"
:href="chartRepo"
target="_blank"
class="js-cluster-application-upgrade-version"
@@ -330,24 +305,13 @@ export default {
</div>
<div
- v-if="upgradeFailed && !isUpgrading"
+ v-if="updateFailed && !isUpgrading"
class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
>
{{ upgradeFailureDescription }}
</div>
-
- <div
- v-if="upgradeRequested && upgradeSuccessful"
- class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
- >
- {{ upgradeSuccessDescription }}
- <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
- &times;
- </button>
- </div>
-
<loading-button
- v-if="upgradeAvailable || upgradeFailed || isUpgrading"
+ v-if="upgradeAvailable || updateFailed || isUpgrading"
class="btn btn-primary js-cluster-application-upgrade-button mt-2"
:loading="isUpgrading"
:disabled="isUpgrading"
@@ -361,9 +325,9 @@ export default {
role="gridcell"
>
<div v-if="showManageButton" class="btn-group table-action-buttons">
- <a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
- manageButtonLabel
- }}</a>
+ <a :href="manageLink" :class="{ disabled: disabled }" class="btn">
+ {{ manageButtonLabel }}
+ </a>
</div>
<div class="btn-group table-action-buttons">
<loading-button
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index ae4fe11c6ae..dfc2069f131 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -224,9 +224,9 @@ export default {
<p class="append-bottom-0">
{{
s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
- Helm Tiller is required to install any of the following applications.`)
+ Helm Tiller is required to install any of the following applications.`)
}}
- <a :href="helpPath"> {{ __('More information') }} </a>
+ <a :href="helpPath">{{ __('More information') }}</a>
</p>
<div class="cluster-application-list prepend-top-10">
@@ -239,15 +239,16 @@ export default {
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
:installed="applications.helm.installed"
+ :install-failed="applications.helm.installFailed"
class="rounded-top"
title-link="https://docs.helm.sh/"
>
<div slot="description">
{{
s__(`ClusterIntegration|Helm streamlines installing
- and managing Kubernetes applications.
- Tiller runs inside of your Kubernetes Cluster,
- and manages releases of your charts.`)
+ and managing Kubernetes applications.
+ Tiller runs inside of your Kubernetes Cluster,
+ and manages releases of your charts.`)
}}
</div>
</application-row>
@@ -255,7 +256,7 @@ export default {
<div class="svg-container" v-html="helmInstallIllustration"></div>
{{
s__(`ClusterIntegration|You must first install Helm Tiller before
- installing the applications below`)
+ installing the applications below`)
}}
</div>
<application-row
@@ -267,6 +268,7 @@ export default {
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed"
+ :install-failed="applications.ingress.installFailed"
:disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
@@ -274,16 +276,14 @@ export default {
<p>
{{
s__(`ClusterIntegration|Ingress gives you a way to route
- requests to services based on the request host or path,
- centralizing a number of services into a single entrypoint.`)
+ requests to services based on the request host or path,
+ centralizing a number of services into a single entrypoint.`)
}}
</p>
<template v-if="ingressInstalled">
<div class="form-group">
- <label for="ingress-endpoint">
- {{ s__('ClusterIntegration|Ingress Endpoint') }}
- </label>
+ <label for="ingress-endpoint">{{ s__('ClusterIntegration|Ingress Endpoint') }}</label>
<div v-if="ingressExternalEndpoint" class="input-group">
<input
id="ingress-endpoint"
@@ -309,8 +309,8 @@ export default {
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
- generated endpoint in order to access
- your application after it has been deployed.`)
+ generated endpoint in order to access
+ your application after it has been deployed.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -321,10 +321,9 @@ export default {
<p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
{{
s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
-
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
@@ -344,6 +343,7 @@ export default {
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
:installed="applications.cert_manager.installed"
+ :install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }"
:disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
@@ -366,15 +366,14 @@ export default {
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Issuers represent a certificate authority.
- You must provide an email address for your Issuer. `)
+ You must provide an email address for your Issuer. `)
}}
<a
href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
target="_blank"
rel="noopener noreferrer"
+ >{{ __('More information') }}</a
>
- {{ __('More information') }}
- </a>
</p>
</div>
</div>
@@ -391,6 +390,7 @@ export default {
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed"
+ :install-failed="applications.prometheus.installFailed"
:disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/"
>
@@ -408,15 +408,18 @@ export default {
:chart-repo="applications.runner.chartRepo"
:upgrade-available="applications.runner.upgradeAvailable"
:installed="applications.runner.installed"
+ :install-failed="applications.runner.installFailed"
+ :update-successful="applications.runner.updateSuccessful"
+ :update-failed="applications.runner.updateFailed"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
<div slot="description">
{{
s__(`ClusterIntegration|GitLab Runner connects to the
- repository and executes CI/CD jobs,
- pushing results back and deploying
- applications to production.`)
+ repository and executes CI/CD jobs,
+ pushing results back and deploying
+ applications to production.`)
}}
</div>
</application-row>
@@ -430,6 +433,7 @@ export default {
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed"
+ :install-failed="applications.jupyter.installFailed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
@@ -438,18 +442,16 @@ export default {
<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.`)
+ 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="ingressExternalEndpoint">
<div class="form-group">
- <label for="jupyter-hostname">
- {{ s__('ClusterIntegration|Jupyter Hostname') }}
- </label>
+ <label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
<div class="input-group">
<input
@@ -470,7 +472,7 @@ export default {
<p v-if="ingressInstalled" class="form-text text-muted">
{{
s__(`ClusterIntegration|Replace this with your own hostname if you want.
- If you do so, point hostname to Ingress IP Address from above.`)
+ If you do so, point hostname to Ingress IP Address from above.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -490,8 +492,10 @@ export default {
:request-status="applications.knative.requestStatus"
:request-reason="applications.knative.requestReason"
:installed="applications.knative.installed"
+ :install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }"
:disabled="!helmInstalled"
+ v-bind="applications.knative"
title-link="https://github.com/knative/docs"
>
<div slot="description">
@@ -499,7 +503,7 @@ export default {
<p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info append-bottom-0">
{{
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
- to install Knative.`)
+ to install Knative.`)
}}
<a :href="helpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -510,9 +514,9 @@ export default {
<p>
{{
s__(`ClusterIntegration|Knative extends Kubernetes to provide
- a set of middleware components that are essential to build modern,
- source-centric, and container-based applications that can run
- anywhere: on premises, in the cloud, or even in a third-party data center.`)
+ a set of middleware components that are essential to build modern,
+ source-centric, and container-based applications that can run
+ anywhere: on premises, in the cloud, or even in a third-party data center.`)
}}
</p>
@@ -523,9 +527,7 @@ export default {
class="form-group col-sm-12 mb-0"
>
<label for="knative-domainname">
- <strong>
- {{ s__('ClusterIntegration|Knative Domain Name:') }}
- </strong>
+ <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<input
id="knative-domainname"
@@ -538,9 +540,7 @@ export default {
<template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint">
- <strong>
- {{ s__('ClusterIntegration|Knative Endpoint:') }}
- </strong>
+ <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
</label>
<div v-if="knativeExternalEndpoint" class="input-group">
<input
@@ -583,8 +583,8 @@ export default {
>
{{
s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
</p>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 17849497c87..48dbce9676e 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -7,6 +7,7 @@ export const CLUSTER_TYPE = {
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
+ NO_STATUS: null,
NOT_INSTALLABLE: 'not_installable',
INSTALLABLE: 'installable',
SCHEDULED: 'scheduled',
@@ -27,17 +28,13 @@ export const APPLICATION_STATUS = {
export const APPLICATION_INSTALLED_STATUSES = [
APPLICATION_STATUS.INSTALLED,
APPLICATION_STATUS.UPDATING,
- APPLICATION_STATUS.UPDATED,
- APPLICATION_STATUS.UPDATE_ERRORED,
- APPLICATION_STATUS.UNINSTALLING,
- APPLICATION_STATUS.UNINSTALL_ERRORED,
];
// These are only used client-side
-export const REQUEST_SUBMITTED = 'request-submitted';
-export const REQUEST_FAILURE = 'request-failure';
-export const UPGRADE_REQUESTED = 'upgrade-requested';
-export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
+
+export const UPDATE_EVENT = 'update';
+export const INSTALL_EVENT = 'install';
+
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
new file mode 100644
index 00000000000..aafb2350ae4
--- /dev/null
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -0,0 +1,141 @@
+import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '../constants';
+
+const {
+ NO_STATUS,
+ SCHEDULED,
+ NOT_INSTALLABLE,
+ INSTALLABLE,
+ INSTALLING,
+ INSTALLED,
+ ERROR,
+ UPDATING,
+ UPDATED,
+ UPDATE_ERRORED,
+} = APPLICATION_STATUS;
+
+const applicationStateMachine = {
+ /* When the application initially loads, it will have `NO_STATUS`
+ * It will transition from `NO_STATUS` once the async backend call is completed
+ */
+ [NO_STATUS]: {
+ on: {
+ [SCHEDULED]: {
+ target: INSTALLING,
+ },
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ },
+ [INSTALLABLE]: {
+ target: INSTALLABLE,
+ },
+ [INSTALLING]: {
+ target: INSTALLING,
+ },
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ [ERROR]: {
+ target: INSTALLABLE,
+ effects: {
+ installFailed: true,
+ },
+ },
+ [UPDATING]: {
+ target: UPDATING,
+ },
+ [UPDATED]: {
+ target: INSTALLED,
+ },
+ [UPDATE_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ updateFailed: true,
+ },
+ },
+ },
+ },
+ [NOT_INSTALLABLE]: {
+ on: {
+ [INSTALLABLE]: {
+ target: INSTALLABLE,
+ },
+ },
+ },
+ [INSTALLABLE]: {
+ on: {
+ [INSTALL_EVENT]: {
+ target: INSTALLING,
+ effects: {
+ installFailed: false,
+ },
+ },
+ // This is possible in artificial environments for E2E testing
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ },
+ },
+ [INSTALLING]: {
+ on: {
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ [ERROR]: {
+ target: INSTALLABLE,
+ effects: {
+ installFailed: true,
+ },
+ },
+ },
+ },
+ [INSTALLED]: {
+ on: {
+ [UPDATE_EVENT]: {
+ target: UPDATING,
+ effects: {
+ updateFailed: false,
+ updateSuccessful: false,
+ },
+ },
+ },
+ },
+ [UPDATING]: {
+ on: {
+ [UPDATED]: {
+ target: INSTALLED,
+ effects: {
+ updateSuccessful: true,
+ updateAcknowledged: false,
+ },
+ },
+ [UPDATE_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ updateFailed: true,
+ },
+ },
+ },
+ },
+};
+
+/**
+ * Determines an application new state based on the application current state
+ * and an event. If the application current state cannot handle a given event,
+ * the current state is returned.
+ *
+ * @param {*} application
+ * @param {*} event
+ */
+const transitionApplicationState = (application, event) => {
+ const newState = applicationStateMachine[application.status].on[event];
+
+ return newState
+ ? {
+ ...application,
+ status: newState.target,
+ ...newState.effects,
+ }
+ : application;
+};
+
+export default transitionApplicationState;
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 38512ac28c2..c2e30960659 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -7,7 +7,11 @@ import {
CERT_MANAGER,
RUNNER,
APPLICATION_INSTALLED_STATUSES,
+ APPLICATION_STATUS,
+ INSTALL_EVENT,
+ UPDATE_EVENT,
} from '../constants';
+import transitionApplicationState from '../services/application_state_machine';
const isApplicationInstalled = appStatus => APPLICATION_INSTALLED_STATUSES.includes(appStatus);
@@ -15,8 +19,8 @@ const applicationInitialState = {
status: null,
statusReason: null,
requestReason: null,
- requestStatus: null,
installed: false,
+ installFailed: false,
};
export default class ClusterStore {
@@ -49,6 +53,9 @@ export default class ClusterStore {
version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
upgradeAvailable: null,
+ updateAcknowledged: true,
+ updateSuccessful: false,
+ updateFailed: false,
},
prometheus: {
...applicationInitialState,
@@ -93,6 +100,32 @@ export default class ClusterStore {
this.state.statusReason = reason;
}
+ installApplication(appId) {
+ this.handleApplicationEvent(appId, INSTALL_EVENT);
+ }
+
+ notifyInstallFailure(appId) {
+ this.handleApplicationEvent(appId, APPLICATION_STATUS.ERROR);
+ }
+
+ updateApplication(appId) {
+ this.handleApplicationEvent(appId, UPDATE_EVENT);
+ }
+
+ notifyUpdateFailure(appId) {
+ this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
+ }
+
+ handleApplicationEvent(appId, event) {
+ const currentAppState = this.state.applications[appId];
+
+ this.state.applications[appId] = transitionApplicationState(currentAppState, event);
+ }
+
+ acknowledgeSuccessfulUpdate(appId) {
+ this.state.applications[appId].updateAcknowledged = true;
+ }
+
updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value;
}
@@ -109,12 +142,16 @@ export default class ClusterStore {
version,
update_available: upgradeAvailable,
} = serverAppEntry;
+ const currentApplicationState = this.state.applications[appId] || {};
+ const nextApplicationState = transitionApplicationState(currentApplicationState, status);
this.state.applications[appId] = {
- ...(this.state.applications[appId] || {}),
- status,
+ ...currentApplicationState,
+ ...nextApplicationState,
statusReason,
- installed: isApplicationInstalled(status),
+ installed: isApplicationInstalled(nextApplicationState.status),
+ // Make sure uninstallable is always false until this feature is unflagged
+ uninstallable: false,
};
if (appId === INGRESS) {
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 777f8fa6691..00eb0afb3bf 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -74,7 +74,7 @@ export default {
<gl-loading-icon
v-if="isLoadingRepos"
class="js-loading-button-icon import-projects-loading-icon"
- :size="4"
+ size="md"
/>
<div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive">
<table class="table import-table">
diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js
index f666e2ebf33..ff1fd1e598e 100644
--- a/app/assets/javascripts/import_projects/store/index.js
+++ b/app/assets/javascripts/import_projects/store/index.js
@@ -7,6 +7,8 @@ import mutations from './mutations';
Vue.use(Vuex);
+export { state, actions, getters, mutations };
+
export default () =>
new Vuex.Store({
state: state(),
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 1b722c0505a..a2ca4b07a66 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -135,6 +135,12 @@ function deferredInitialisation() {
});
loadAwardsHandler();
+
+ // Toggle Canary Badge
+ if (Cookies.get('gitlab_canary') && Cookies.get('gitlab_canary') === 'true') {
+ document.querySelector('.js-canary-badge').classList.remove('hidden');
+ document.querySelector('.js-canary-link').classList.add('hidden');
+ }
}
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 61204c37307..4a20753e7ae 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -65,18 +65,18 @@ export default class ActivityCalendar {
this.daySize = 15;
this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Oct',
- 'Nov',
- 'Dec',
+ __('Jan'),
+ __('Feb'),
+ __('Mar'),
+ __('Apr'),
+ __('May'),
+ __('Jun'),
+ __('Jul'),
+ __('Aug'),
+ __('Sep'),
+ __('Oct'),
+ __('Nov'),
+ __('Dec'),
];
this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 6660f8120f8..b8976f77bac 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -34,6 +34,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
+ mediator: this.mediator,
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index aa4ecb0aac3..705ee05e29f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -119,7 +119,8 @@ export default {
},
showTargetBranchAdvancedError() {
return Boolean(
- this.mr.pipeline &&
+ this.mr.isOpen &&
+ this.mr.pipeline &&
this.mr.pipeline.target_sha &&
this.mr.pipeline.target_sha !== this.mr.targetBranchSha,
);
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index 1f9670cf2fc..53e6efa6ea3 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -17,15 +17,13 @@ export default {
required: true,
},
},
- data() {
- return {
- milestoneDue: this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null,
- milestoneStart: this.milestone.start_date
- ? parsePikadayDate(this.milestone.start_date)
- : null,
- };
- },
computed: {
+ milestoneDue() {
+ return this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null;
+ },
+ milestoneStart() {
+ return this.milestone.start_date ? parsePikadayDate(this.milestone.start_date) : null;
+ },
isMilestoneStarted() {
if (!this.milestoneStart) {
return false;
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index ffde55bf083..b807a35b421 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -1,4 +1,5 @@
<script>
+import '~/commons/bootstrap';
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale';
import IssueMilestone from '../../components/issue/issue_milestone.vue';
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
index cafd3a515ea..c09bdfec250 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
@@ -17,10 +17,10 @@ export default {
<template>
<tr class="line_holder" :class="lineType">
- <td class="diff-line-num old_line" :class="lineType">
+ <td class="diff-line-num old_line border-top-0 border-bottom-0" :class="lineType">
{{ line.old_line }}
</td>
- <td class="diff-line-num new_line" :class="lineType">
+ <td class="diff-line-num new_line border-top-0 border-bottom-0" :class="lineType">
{{ line.new_line }}
</td>
<td class="line_content" :class="lineType">
diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue
index 3074ea859cc..6d2612556ff 100644
--- a/app/assets/javascripts/vue_shared/components/select2_select.vue
+++ b/app/assets/javascripts/vue_shared/components/select2_select.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import 'select2/select2';
+import 'select2';
export default {
name: 'Select2Select',
diff --git a/app/assets/stylesheets/components/toast.scss b/app/assets/stylesheets/components/toast.scss
new file mode 100644
index 00000000000..91d16c8e98d
--- /dev/null
+++ b/app/assets/stylesheets/components/toast.scss
@@ -0,0 +1,3 @@
+.toast-close {
+ font-size: $default-icon-size !important;
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1e025b3a67d..a6179e2a96e 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -447,30 +447,29 @@
}
}
+.title-container,
.navbar-nav {
- li {
- .badge.badge-pill {
- position: inherit;
- font-weight: $gl-font-weight-normal;
- margin-left: -6px;
- font-size: 11px;
- color: $white-light;
- padding: 0 5px;
- line-height: 12px;
- border-radius: 7px;
- box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
-
- &.issues-count {
- background-color: $green-500;
- }
+ .badge.badge-pill {
+ position: inherit;
+ font-weight: $gl-font-weight-normal;
+ margin-left: -6px;
+ font-size: 11px;
+ color: $white-light;
+ padding: 0 5px;
+ line-height: 12px;
+ border-radius: 7px;
+ box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
+
+ &.green-badge {
+ background-color: $green-500;
+ }
- &.merge-requests-count {
- background-color: $orange-600;
- }
+ &.merge-requests-count {
+ background-color: $orange-600;
+ }
- &.todos-count {
- background-color: $blue-500;
- }
+ &.todos-count {
+ background-color: $blue-500;
}
}
}
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index a9b85889846..319d0307f78 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -17,6 +17,8 @@
- if logo_text.present?
%span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text
+ %span.js-canary-badge.badge.badge-pill.green-badge.align-self-center
+ = _('Next')
- if current_user
= render "layouts/nav/dashboard"
@@ -38,7 +40,7 @@
= link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues)
- %span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) }
+ %span.badge.badge-pill.issues-count.green-badge{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index cd9128c452b..c53bfd8a85d 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -7,3 +7,6 @@
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
= render 'shared/user_dropdown_contributing_link'
+ - if Gitlab.com?
+ %li.js-canary-link
+ = link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 50adc19f524..3ca4abddbb8 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -80,6 +80,8 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
+ = render_if_exists "projects/home_mirror"
+
- if @project.badges.present?
.project-badges.mb-2
- @project.badges.each do |badge|
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 79c586eef73..05aeb5d972b 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -37,21 +37,21 @@
%ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%li.notes-tab.qa-notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do
- Discussion
+ = _("Discussion")
%span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= tab_link_for @merge_request, :commits do
- Commits
+ = _("Commits")
%span.badge.badge-pill= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= tab_link_for @merge_request, :pipelines do
- Pipelines
+ = _("Pipelines")
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab.qa-diffs-tab
= tab_link_for @merge_request, :diffs do
- Changes
+ = _("Changes")
%span.badge.badge-pill= @merge_request.diff_size
.d-inline-flex.flex-wrap
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index c031815200b..a1ec2c887c2 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -11,7 +11,7 @@
= link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank'
.settings-content
- = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'false', data: mirrors_form_data_attributes } do |f|
+ = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'new-password', data: mirrors_form_data_attributes } do |f|
.panel.panel-default
.panel-heading
%h3.panel-title= _('Mirror a repository')
@@ -20,7 +20,7 @@
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
- = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+"
+ = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password'
= render 'projects/mirrors/instructions'
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 5b25a67bc87..8ae2807729b 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -21,10 +21,9 @@
- if @scope == 'projects'
.term
= render 'shared/projects/list', projects: @search_objects, pipeline_status: false
- - elsif %w[blobs wiki_blobs].include?(@scope)
- = render partial: 'search/results/blob', collection: @search_objects, locals: { projects: blob_projects(@search_objects) }
- else
- = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
+ - locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope)
+ = render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals
- if @scope != 'projects'
= paginate_collection(@search_objects)
diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index aa428f9fe73..d90a6d43761 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -1,8 +1,8 @@
%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('angle-double-left', css_class: 'icon-angle-double-left')
= sprite_icon('angle-double-right', css_class: 'icon-angle-double-right')
- %span.collapse-text _("Collapse sidebar")
+ %span.collapse-text= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do
= sprite_icon('close', size: 16)
- %span.collapse-text _("Close sidebar")
+ %span.collapse-text= _("Close sidebar")
diff --git a/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml b/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml
new file mode 100644
index 00000000000..89d2fced6e1
--- /dev/null
+++ b/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Sidekiq Reliable Fetcher for background jobs by default
+merge_request: 27530
+author:
+type: added
diff --git a/changelogs/unreleased/46048-canary-next.yml b/changelogs/unreleased/46048-canary-next.yml
new file mode 100644
index 00000000000..1a702cccff9
--- /dev/null
+++ b/changelogs/unreleased/46048-canary-next.yml
@@ -0,0 +1,5 @@
+---
+title: Adds badge for Canary environment and help link
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/57017-add-toast-success-message.yml b/changelogs/unreleased/57017-add-toast-success-message.yml
new file mode 100644
index 00000000000..931e7755591
--- /dev/null
+++ b/changelogs/unreleased/57017-add-toast-success-message.yml
@@ -0,0 +1,5 @@
+---
+title: Display a toast message when the Kubernetes runner has successfully upgraded.
+merge_request: 27206
+author:
+type: changed
diff --git a/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml b/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml
new file mode 100644
index 00000000000..b340f8408f3
--- /dev/null
+++ b/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml
@@ -0,0 +1,6 @@
+---
+title: Only show the "target branch has advanced" message when the merge request is
+ open
+merge_request: 27588
+author:
+type: fixed
diff --git a/changelogs/unreleased/60874-fix-suggestion-misalignment.yml b/changelogs/unreleased/60874-fix-suggestion-misalignment.yml
new file mode 100644
index 00000000000..f5717ac19fd
--- /dev/null
+++ b/changelogs/unreleased/60874-fix-suggestion-misalignment.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Misalignment on suggested changes diff table
+merge_request: 27612
+author:
+type: fixed
diff --git a/changelogs/unreleased/60906-fix-wiki-links.yml b/changelogs/unreleased/60906-fix-wiki-links.yml
new file mode 100644
index 00000000000..cc65a1382bf
--- /dev/null
+++ b/changelogs/unreleased/60906-fix-wiki-links.yml
@@ -0,0 +1,5 @@
+---
+title: Show proper wiki links in search results
+merge_request: 27634
+author:
+type: fixed
diff --git a/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml b/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml
new file mode 100644
index 00000000000..03d94c39c10
--- /dev/null
+++ b/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: 'refactor(issue): Refactored issue tests from Karma to Jest'
+merge_request: 27673
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml b/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml
new file mode 100644
index 00000000000..9a1886797da
--- /dev/null
+++ b/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: 'Refactored notes tests from Karma to Jest'
+merge_request: 27648
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml b/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml
new file mode 100644
index 00000000000..e855684bab1
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml
@@ -0,0 +1,5 @@
+---
+title: Disable password autocomplete in mirror repository form
+merge_request: 27542
+author:
+type: fixed
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 2e4aa9c1053..7b69cf11288 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,5 +1,17 @@
require 'sidekiq/web'
+def enable_reliable_fetch?
+ return true unless Feature::FlipperFeature.table_exists?
+
+ Feature.enabled?(:gitlab_sidekiq_reliable_fetcher, default_enabled: true)
+end
+
+def enable_semi_reliable_fetch_mode?
+ return true unless Feature::FlipperFeature.table_exists?
+
+ Feature.enabled?(:gitlab_sidekiq_enable_semi_reliable_fetcher, default_enabled: true)
+end
+
# Disable the Sidekiq Rack session since GitLab already has its own session store.
# CSRF protection still works (https://github.com/mperham/sidekiq/commit/315504e766c4fd88a29b7772169060afc4c40329).
Sidekiq::Web.set :sessions, false
@@ -45,9 +57,8 @@ Sidekiq.configure_server do |config|
ActiveRecord::Base.clear_all_connections!
end
- if Feature::FlipperFeature.table_exists? && Feature.enabled?(:gitlab_sidekiq_reliable_fetcher)
- # By default we're going to use Semi Reliable Fetch
- config.options[:semi_reliable_fetch] = Feature.enabled?(:gitlab_sidekiq_enable_semi_reliable_fetcher, default_enabled: true)
+ if enable_reliable_fetch?
+ config.options[:semi_reliable_fetch] = enable_semi_reliable_fetch_mode?
Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
end
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 223585ebb55..38d56322de8 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -300,7 +300,7 @@ file in. Once the changes are on master, they will be picked up by
[Crowdin](http://translate.gitlab.com) and be presented for translation.
If there are merge conflicts in the `gitlab.pot` file, you can delete the file
-and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
+and regenerate it using the same command.
### Validating PO files
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 06f2f848925..5bc9bb3434d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1890,6 +1890,9 @@ msgstr ""
msgid "Close milestone"
msgstr ""
+msgid "Close sidebar"
+msgstr ""
+
msgid "Closed"
msgstr ""
@@ -3259,6 +3262,9 @@ msgstr ""
msgid "Discuss a specific suggestion or question that needs to be resolved"
msgstr ""
+msgid "Discussion"
+msgstr ""
+
msgid "Dismiss"
msgstr ""
@@ -5936,6 +5942,9 @@ msgstr ""
msgid "Newly registered users will by default be external"
msgstr ""
+msgid "Next"
+msgstr ""
+
msgid "No"
msgstr ""
@@ -8671,6 +8680,9 @@ msgstr ""
msgid "Switch branch/tag"
msgstr ""
+msgid "Switch to GitLab Next"
+msgstr ""
+
msgid "System Hooks"
msgstr ""
diff --git a/package.json b/package.json
index e04470109be..d2ee29fe06d 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"@babel/preset-env": "^7.3.1",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.59.0",
- "@gitlab/ui": "^3.4.0",
+ "@gitlab/ui": "^3.5.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-upload-client": "^10.0.0",
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 9c60f0fcd4d..4634d1d4bb3 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -11,6 +11,30 @@ describe SearchController do
sign_in(user)
end
+ context 'uses the right partials depending on scope' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
+
+ set(:project) { create(:project, :public, :repository, :wiki_repo) }
+
+ subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
+
+ where(:partial, :scope) do
+ '_blob' | :blobs
+ '_wiki_blob' | :wiki_blobs
+ '_commit' | :commits
+ end
+
+ with_them do
+ it do
+ project_wiki = create(:project_wiki, project: project, user: user)
+ create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
+
+ expect(subject).to render_template("search/results/#{partial}")
+ end
+ end
+ end
+
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index 8eaccfc0949..cc04798248c 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -1,6 +1,6 @@
require "rails_helper"
-describe "Jira", :js do
+describe "Jira", :js, :quarantine do
let(:user) { create(:user) }
let(:actual_project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) }
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 0aff916ec83..0dbff5a2701 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Branches', :js do
+ include ProtectedBranchHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
@@ -150,27 +152,11 @@ describe 'Protected Branches', :js do
end
describe "access control" do
- include_examples "protected branches > access control > CE"
- end
- end
-
- def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").click
- find(".dropdown-input-field").set(branch_name)
- click_on("Create wildcard #{branch_name}")
- end
-
- def set_defaults
- find(".js-allowed-to-merge").click
- within('.qa-allowed-to-merge-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
- find(".js-allowed-to-push").click
- within('.qa-allowed-to-push-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
+ include_examples "protected branches > access control > CE"
end
end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index c8e92cd1c07..652542b1719 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Tags', :js do
+ include ProtectedTagHelpers
+
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
@@ -8,13 +10,6 @@ describe 'Protected Tags', :js do
sign_in(user)
end
- def set_protected_tag_name(tag_name)
- find(".js-protected-tag-select").click
- find(".dropdown-input-field").set(tag_name)
- click_on("Create wildcard #{tag_name}")
- find('.protected-tags-dropdown .dropdown-menu', visible: false)
- end
-
describe "explicit protected tags" do
it "allows creating explicit protected tags" do
visit project_protected_tags_path(project)
@@ -92,6 +87,10 @@ describe 'Protected Tags', :js do
end
describe "access control" do
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
+
include_examples "protected tags > access control > CE"
end
end
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 33a35069004..5103cb4f69f 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,16 +1,13 @@
import Clusters from '~/clusters/clusters_bundle';
-import {
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- APPLICATION_STATUS,
- INGRESS_DOMAIN_SUFFIX,
-} from '~/clusters/constants';
+import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX } from '~/clusters/constants';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import $ from 'jquery';
+const { INSTALLING, INSTALLABLE, INSTALLED, NOT_INSTALLABLE } = APPLICATION_STATUS;
+
describe('Clusters', () => {
setTestTimeout(1000);
@@ -93,7 +90,7 @@ describe('Clusters', () => {
it('does not show alert when things transition from initial null state to something', () => {
cluster.checkForNewInstalls(INITIAL_APP_MAP, {
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Helm Tiller' },
+ helm: { status: INSTALLABLE, title: 'Helm Tiller' },
});
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
@@ -105,11 +102,11 @@ describe('Clusters', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
},
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
},
);
@@ -125,13 +122,13 @@ describe('Clusters', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Ingress' },
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
+ ingress: { status: INSTALLABLE, title: 'Ingress' },
},
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLED, title: 'Ingress' },
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
+ ingress: { status: INSTALLED, title: 'Ingress' },
},
);
@@ -218,11 +215,11 @@ describe('Clusters', () => {
it('tries to install helm', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+ cluster.store.state.applications.helm.status = INSTALLABLE;
cluster.installApplication({ id: 'helm' });
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
});
@@ -230,11 +227,11 @@ describe('Clusters', () => {
it('tries to install ingress', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
+ cluster.store.state.applications.ingress.status = INSTALLABLE;
cluster.installApplication({ id: 'ingress' });
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.ingress.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
});
@@ -242,11 +239,11 @@ describe('Clusters', () => {
it('tries to install runner', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
+ cluster.store.state.applications.runner.status = INSTALLABLE;
cluster.installApplication({ id: 'runner' });
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.runner.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
});
@@ -254,13 +251,12 @@ describe('Clusters', () => {
it('tries to install jupyter', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
cluster.installApplication({
id: 'jupyter',
params: { hostname: cluster.store.state.applications.jupyter.hostname },
});
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUBMITTED);
+ cluster.store.state.applications.jupyter.status = INSTALLABLE;
expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', {
hostname: cluster.store.state.applications.jupyter.hostname,
@@ -272,16 +268,18 @@ describe('Clusters', () => {
.spyOn(cluster.service, 'installApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR'));
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+ cluster.store.state.applications.helm.status = INSTALLABLE;
const promise = cluster.installApplication({ id: 'helm' });
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalled();
return promise.then(() => {
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
+ expect(cluster.store.state.applications.helm.installFailed).toBe(true);
+
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
@@ -315,7 +313,6 @@ describe('Clusters', () => {
});
describe('toggleIngressDomainHelpText', () => {
- const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS;
let ingressPreviousState;
let ingressNewState;
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index 038d2be9e98..17273b7d5b1 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import eventHub from '~/clusters/event_hub';
-import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants';
+import { APPLICATION_STATUS } from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
@@ -80,17 +80,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(false);
});
- it('has loading "Installing" when APPLICATION_STATUS.SCHEDULED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.SCHEDULED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -102,18 +91,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true);
});
- it('has loading "Installing" when REQUEST_SUBMITTED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_SUBMITTED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
it('has disabled "Installed" when application is installed and not uninstallable', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -139,10 +116,11 @@ describe('Application Row', () => {
expect(installBtn).toBe(null);
});
- it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => {
+ it('has enabled "Install" when install fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.ERROR,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -154,7 +132,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -246,15 +223,15 @@ describe('Application Row', () => {
expect(upgradeBtn.innerHTML).toContain('Upgrade');
});
- it('has enabled "Retry update" when APPLICATION_STATUS.UPDATE_ERRORED', () => {
+ it('has enabled "Retry update" when update process fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).not.toBe(null);
- expect(vm.upgradeFailed).toBe(true);
expect(upgradeBtn.innerHTML).toContain('Retry update');
});
@@ -274,7 +251,8 @@ describe('Application Row', () => {
jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ upgradeAvailable: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
@@ -303,7 +281,8 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner',
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
const failureMessage = vm.$el.querySelector(
'.js-cluster-application-upgrade-failure-message',
@@ -314,6 +293,21 @@ describe('Application Row', () => {
'Update failed. Please check the logs and try again.',
);
});
+
+ it('displays a success toast message if application upgrade was successful', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ title: 'GitLab Runner',
+ updateSuccessful: false,
+ });
+
+ vm.$toast = { show: jest.fn() };
+ vm.updateSuccessful = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.');
+ });
+ });
});
describe('Version', () => {
@@ -321,7 +315,8 @@ describe('Application Row', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
@@ -337,7 +332,8 @@ describe('Application Row', () => {
const chartRepo = 'https://gitlab.com/charts/gitlab-runner';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
chartRepo,
version,
});
@@ -351,7 +347,8 @@ describe('Application Row', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
@@ -367,7 +364,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: null,
- requestStatus: null,
});
const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message',
@@ -376,12 +372,13 @@ describe('Application Row', () => {
expect(generalErrorMessage).toBeNull();
});
- it('shows status reason when APPLICATION_STATUS.ERROR', () => {
+ it('shows status reason when install fails', () => {
const statusReason = 'We broke it 0.0';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.ERROR,
statusReason,
+ installFailed: true,
});
const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message',
@@ -402,7 +399,7 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
+ installFailed: true,
requestReason,
});
const generalErrorMessage = vm.$el.querySelector(
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
new file mode 100644
index 00000000000..e74b7910572
--- /dev/null
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -0,0 +1,134 @@
+import transitionApplicationState from '~/clusters/services/application_state_machine';
+import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '~/clusters/constants';
+
+const {
+ NO_STATUS,
+ SCHEDULED,
+ NOT_INSTALLABLE,
+ INSTALLABLE,
+ INSTALLING,
+ INSTALLED,
+ ERROR,
+ UPDATING,
+ UPDATED,
+ UPDATE_ERRORED,
+} = APPLICATION_STATUS;
+
+const NO_EFFECTS = 'no effects';
+
+describe('applicationStateMachine', () => {
+ const noEffectsToEmptyObject = effects => (typeof effects === 'string' ? {} : effects);
+
+ describe(`current state is ${NO_STATUS}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ ${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NO_STATUS,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${NOT_INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NOT_INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLED}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLED,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+
+ describe(`current state is ${UPDATING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true, updateAcknowledged: false }}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: UPDATING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index b4d1bb710e0..1e896af1c7d 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -113,7 +113,6 @@ const DEFAULT_APPLICATION_STATE = {
description: 'Some description about this interesting application!',
status: null,
statusReason: null,
- requestStatus: null,
requestReason: null,
};
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index c0e8b737ea2..a20e0439555 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -32,15 +32,6 @@ describe('Clusters Store', () => {
});
describe('updateAppProperty', () => {
- it('should store new request status', () => {
- expect(store.state.applications.helm.requestStatus).toEqual(null);
-
- const newStatus = APPLICATION_STATUS.INSTALLING;
- store.updateAppProperty('helm', 'requestStatus', newStatus);
-
- expect(store.state.applications.helm.requestStatus).toEqual(newStatus);
- });
-
it('should store new request reason', () => {
expect(store.state.applications.helm.requestReason).toEqual(null);
@@ -68,80 +59,90 @@ describe('Clusters Store', () => {
title: 'Helm Tiller',
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
- requestStatus: null,
requestReason: null,
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
ingress: {
title: 'Ingress',
- status: mockResponseData.applications[1].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[1].status_reason,
- requestStatus: null,
requestReason: null,
externalIp: null,
externalHostname: null,
installed: false,
+ installFailed: true,
+ uninstallable: false,
},
runner: {
title: 'GitLab Runner',
status: mockResponseData.applications[2].status,
statusReason: mockResponseData.applications[2].status_reason,
- requestStatus: null,
requestReason: null,
version: mockResponseData.applications[2].version,
upgradeAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
installed: false,
+ installFailed: false,
+ updateAcknowledged: true,
+ updateFailed: false,
+ updateSuccessful: false,
+ uninstallable: false,
},
prometheus: {
title: 'Prometheus',
- status: mockResponseData.applications[3].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason,
- requestStatus: null,
requestReason: null,
installed: false,
+ installFailed: true,
+ uninstallable: false,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason,
- requestStatus: null,
requestReason: null,
hostname: '',
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
knative: {
title: 'Knative',
status: mockResponseData.applications[5].status,
statusReason: mockResponseData.applications[5].status_reason,
- requestStatus: null,
requestReason: null,
hostname: null,
isEditingHostName: false,
externalIp: null,
externalHostname: null,
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
cert_manager: {
title: 'Cert-Manager',
- status: mockResponseData.applications[6].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
statusReason: mockResponseData.applications[6].status_reason,
- requestStatus: null,
requestReason: null,
email: mockResponseData.applications[6].email,
installed: false,
+ uninstallable: false,
},
},
});
});
- describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', () => {
+ describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', status => {
it('marks application as installed', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
const runnerAppIndex = 2;
- mockResponseData.applications[runnerAppIndex].status = APPLICATION_STATUS.INSTALLED;
+ mockResponseData.applications[runnerAppIndex].status = status;
store.updateStateFromServer(mockResponseData);
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
new file mode 100644
index 00000000000..17a998d0174
--- /dev/null
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -0,0 +1,185 @@
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
+import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
+import STATUS_MAP from '~/import_projects/constants';
+
+describe('ImportProjectsTable', () => {
+ let vm;
+ const providerTitle = 'THE PROVIDER';
+ const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
+ const importedProject = {
+ id: 1,
+ fullPath: 'fullPath',
+ importStatus: 'started',
+ providerLink: 'providerLink',
+ importSource: 'importSource',
+ };
+
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchJobs: jest.fn(),
+ fetchRepos: jest.fn(actions.requestRepos),
+ fetchImport: jest.fn(actions.requestImport),
+ });
+
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+
+ const component = mount(importProjectsTable, {
+ localVue,
+ store,
+ propsData: {
+ providerTitle,
+ },
+ sync: false,
+ });
+
+ return component.vm;
+ }
+
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a loading icon whilst repos are loading', () =>
+ vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
+ }));
+
+ it('renders a table with imported projects and provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).not.toBeNull();
+ expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
+ `From ${providerTitle}`,
+ );
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+ });
+ });
+
+ it('renders an empty state if there are no imported projects or provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [],
+ namespaces: [],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).toBeNull();
+ expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
+ });
+ });
+
+ it('shows loading spinner when bulk import button is clicked', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$el.querySelector('.js-import-all').click();
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-import-all .js-loading-button-icon')).not.toBeNull();
+ });
+ });
+
+ it('imports provider repos if bulk import button is clicked', () => {
+ mountComponent();
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$store.dispatch('receiveImportSuccess', { importedProject, repoId: providerRepo.id });
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
+ });
+ });
+
+ it('polls to update the status of imported projects', () => {
+ const updatedProjects = [
+ {
+ id: importedProject.id,
+ importStatus: 'finished',
+ },
+ ];
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ const statusObject = STATUS_MAP[importedProject.importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+
+ vm.$store.dispatch('receiveJobsSuccess', updatedProjects);
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
index 7dac7e9ccc1..f95acc1edd7 100644
--- a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js
+++ b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
@@ -1,5 +1,6 @@
-import Vue from 'vue';
+import Vuex from 'vuex';
import createStore from '~/import_projects/store';
+import { createLocalVue, mount } from '@vue/test-utils';
import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import STATUS_MAP from '~/import_projects/constants';
@@ -13,27 +14,33 @@ describe('ImportedProjectTableRow', () => {
importSource: 'importSource',
};
- function createComponent() {
- const ImportedProjectTableRow = Vue.extend(importedProjectTableRow);
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
- const store = createStore();
- return new ImportedProjectTableRow({
- store,
+ const component = mount(importedProjectTableRow, {
+ localVue,
+ store: createStore(),
propsData: {
project: {
...project,
},
},
- }).$mount();
+ sync: false,
+ });
+
+ return component.vm;
}
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
afterEach(() => {
vm.$destroy();
});
it('renders an imported project table row', () => {
- vm = createComponent();
-
const providerLink = vm.$el.querySelector('.js-provider-link');
const statusObject = STATUS_MAP[project.importStatus];
diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index 4d2bacd2ad0..02c786d8d0b 100644
--- a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -1,14 +1,15 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/import_projects/store';
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
describe('ProviderRepoTableRow', () => {
- let store;
let vm;
+ const fetchImport = jest.fn((context, data) => actions.requestImport(context, data));
+ const importPath = '/import-path';
+ const defaultTargetNamespace = 'user';
+ const ciCdOnly = true;
const repo = {
id: 10,
sanitizedName: 'sanitizedName',
@@ -16,21 +17,42 @@ describe('ProviderRepoTableRow', () => {
providerLink: 'providerLink',
};
- function createComponent() {
- const ProviderRepoTableRow = Vue.extend(providerRepoTableRow);
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchImport,
+ });
- return new ProviderRepoTableRow({
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+ store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
+
+ const component = mount(providerRepoTableRow, {
+ localVue,
store,
propsData: {
- repo: {
- ...repo,
- },
+ repo,
},
- }).$mount();
+ sync: false,
+ });
+
+ return component.vm;
}
beforeEach(() => {
- store = createStore();
+ vm = mountComponent();
});
afterEach(() => {
@@ -38,8 +60,6 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a provider repo table row', () => {
- vm = createComponent();
-
const providerLink = vm.$el.querySelector('.js-provider-link');
const statusObject = STATUS_MAP[STATUSES.NONE];
@@ -55,8 +75,6 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a select2 namespace select', () => {
- vm = createComponent();
-
const dropdownTrigger = vm.$el.querySelector('.js-namespace-select');
expect(dropdownTrigger).not.toBeNull();
@@ -67,30 +85,20 @@ describe('ProviderRepoTableRow', () => {
expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
});
- it('imports repo when clicking import button', done => {
- const importPath = '/import-path';
- const defaultTargetNamespace = 'user';
- const ciCdOnly = true;
- const mock = new MockAdapter(axios);
-
- store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
- mock.onPost(importPath).replyOnce(200);
- spyOn(store, 'dispatch').and.returnValue(new Promise(() => {}));
-
- vm = createComponent();
-
+ it('imports repo when clicking import button', () => {
vm.$el.querySelector('.js-import-button').click();
- setTimeoutPromise()
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchImport', {
- repo,
- newName: repo.sanitizedName,
- targetNamespace: defaultTargetNamespace,
- });
- })
- .then(() => mock.restore())
- .then(done)
- .catch(done.fail);
+ return vm.$nextTick().then(() => {
+ const { calls } = fetchImport.mock;
+
+ // Not using .toBeCalledWith because it expects
+ // an unmatchable and undefined 3rd argument.
+ expect(calls.length).toBe(1);
+ expect(calls[0][1]).toEqual({
+ repo,
+ newName: repo.sanitizedName,
+ targetNamespace: defaultTargetNamespace,
+ });
+ });
});
});
diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 77850ee3283..6a7b90788dd 100644
--- a/spec/javascripts/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -27,8 +27,8 @@ import {
stopJobsPolling,
} from '~/import_projects/store/actions';
import state from '~/import_projects/store/state';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
describe('import_projects store actions', () => {
let localState;
diff --git a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 9eac75fac96..d1de98f4a15 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -2,8 +2,8 @@ import Vue from 'vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockAssigneesList } from 'spec/boards/mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockAssigneesList } from '../../../../javascripts/boards/mock_data';
const createComponent = (assignees = mockAssigneesList, cssClass = '') => {
const Component = Vue.extend(IssueAssignees);
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
new file mode 100644
index 00000000000..2e93ec412b9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+
+import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
+
+import { mockMilestone } from '../../../../javascripts/boards/mock_data';
+
+const createComponent = (milestone = mockMilestone) => {
+ const Component = Vue.extend(IssueMilestone);
+
+ return mount(Component, {
+ propsData: {
+ milestone,
+ },
+ sync: false,
+ });
+};
+
+describe('IssueMilestoneComponent', () => {
+ let wrapper;
+ let vm;
+
+ beforeEach(done => {
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('isMilestoneStarted', () => {
+ it('should return `false` when milestoneStart prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(false);
+ });
+
+ it('should return `true` when milestone start date is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(true);
+ });
+ });
+
+ describe('isMilestonePastDue', () => {
+ it('should return `false` when milestoneDue prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(false);
+ });
+
+ it('should return `true` when milestone due is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(true);
+ });
+ });
+
+ describe('milestoneDatesAbsolute', () => {
+ it('returns string containing absolute milestone due date', () => {
+ expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
+ });
+
+ it('returns string containing absolute milestone start date when due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ });
+
+ it('returns empty string when both milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
+ });
+ });
+
+ describe('milestoneDatesHuman', () => {
+ it('returns string containing milestone due date when date is yet to be due', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: `${new Date().getFullYear() + 10}-01-01`,
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
+ });
+
+ it('returns string containing milestone start date when date has already started and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
+ });
+
+ it('returns string containing milestone start date when date is yet to start and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: `${new Date().getFullYear() + 10}-01-01`,
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
+ });
+
+ it('returns empty string when milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toBe('');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-milestone-details`', () => {
+ expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
+ });
+
+ it('renders milestone icon', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
+ });
+
+ it('renders milestone title', () => {
+ expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
+ });
+
+ it('renders milestone tooltip', () => {
+ expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
+ mockMilestone.title,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
index aa7d6ea2e34..4a8de5fc4f1 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const IssueWarning = Vue.extend(issueWarning);
@@ -19,7 +19,9 @@ describe('Issue Warning Component', () => {
isLocked: true,
});
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/);
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/lock$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
'This issue is locked. Only project members can comment.',
);
@@ -32,7 +34,9 @@ describe('Issue Warning Component', () => {
isConfidential: true,
});
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/);
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/eye-slash$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
'This is a confidential issue. Your comment will not be visible to the public.',
);
diff --git a/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index 42198e92eea..e43d5301a50 100644
--- a/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -1,7 +1,11 @@
import Vue from 'vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
import { mount, createLocalVue } from '@vue/test-utils';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-import { defaultMilestone, defaultAssignees } from './related_issuable_mock_data';
+import {
+ defaultAssignees,
+ defaultMilestone,
+} from '../../../../javascripts/vue_shared/components/issue/related_issuable_mock_data';
describe('RelatedIssuableItem', () => {
let wrapper;
@@ -85,11 +89,11 @@ describe('RelatedIssuableItem', () => {
it('renders state title', () => {
const stateTitle = tokenState.attributes('data-original-title');
+ const formatedCreateDate = formatDate(props.createdAt);
expect(stateTitle).toContain('<span class="bold">Opened</span>');
- expect(stateTitle).toContain(
- '<span class="text-tertiary">Dec 1, 2018 12:00am GMT+0000</span>',
- );
+
+ expect(stateTitle).toContain(`<span class="text-tertiary">${formatedCreateDate}</span>`);
});
it('renders aria label', () => {
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index 45f131194ca..eafff7f681e 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import createStore from '~/notes/stores';
-import { userDataMock } from '../../../notes/mock_data';
+import { userDataMock } from '../../../../javascripts/notes/mock_data';
describe('issue placeholder system note component', () => {
let store;
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
index 6013e85811a..976e38c15ee 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('placeholder system note component', () => {
let PlaceholderSystemNote;
diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index adcb1c858aa..adcb1c858aa 100644
--- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js
deleted file mode 100644
index ab8642bf0dd..00000000000
--- a/spec/javascripts/import_projects/components/import_projects_table_spec.js
+++ /dev/null
@@ -1,188 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/import_projects/store';
-import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
-import STATUS_MAP from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
-
-describe('ImportProjectsTable', () => {
- let vm;
- let mock;
- let store;
- const reposPath = '/repos-path';
- const jobsPath = '/jobs-path';
- const providerTitle = 'THE PROVIDER';
- const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
- const importedProject = {
- id: 1,
- fullPath: 'fullPath',
- importStatus: 'started',
- providerLink: 'providerLink',
- importSource: 'importSource',
- };
-
- function createComponent() {
- const ImportProjectsTable = Vue.extend(importProjectsTable);
-
- const component = new ImportProjectsTable({
- store,
- propsData: {
- providerTitle,
- },
- }).$mount();
-
- store.dispatch('stopJobsPolling');
-
- return component;
- }
-
- beforeEach(() => {
- store = createStore();
- store.dispatch('setInitialData', { reposPath });
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- it('renders a loading icon whilst repos are loading', done => {
- mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders a table with imported projects and provider repos', done => {
- const response = {
- importedProjects: [importedProject],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).not.toBeNull();
- expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
- `From ${providerTitle}`,
- );
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders an empty state if there are no imported projects or provider repos', done => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- namespaces: [],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).toBeNull();
- expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('imports provider repos if bulk import button is clicked', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
-
- mock.onGet(reposPath).replyOnce(200, response);
- mock.onPost(importPath).replyOnce(200, importedProject);
-
- store.dispatch('setInitialData', { importPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
-
- vm.$el.querySelector('.js-import-all').click();
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('polls to update the status of imported projects', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [importedProject],
- providerRepos: [],
- namespaces: [{ path: 'path' }],
- };
- const updatedProjects = [
- {
- id: importedProject.id,
- importStatus: 'finished',
- },
- ];
-
- mock.onGet(reposPath).replyOnce(200, response);
-
- store.dispatch('setInitialData', { importPath, jobsPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- const statusObject = STATUS_MAP[importedProject.importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
-
- mock.onGet(jobsPath).replyOnce(200, updatedProjects);
- return store.dispatch('restartJobsPolling');
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 690fcd3e224..a0628fdcebe 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -228,6 +228,7 @@ describe('mrWidgetOptions', () => {
describe('showTargetBranchAdvancedError', () => {
describe(`when the pipeline's target_sha property doesn't exist`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', undefined);
Vue.set(vm.mr, 'targetBranchSha', 'abcd');
vm.$nextTick(done);
@@ -240,6 +241,7 @@ describe('mrWidgetOptions', () => {
describe(`when the pipeline's target_sha matches the target branch's sha`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
Vue.set(vm.mr, 'targetBranchSha', 'abcd');
vm.$nextTick(done);
@@ -250,8 +252,22 @@ describe('mrWidgetOptions', () => {
});
});
+ describe(`when the merge request is not open`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', false);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'bcde');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
describe(`when the pipeline's target_sha does not match the target branch's sha`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
Vue.set(vm.mr, 'targetBranchSha', 'bcde');
vm.$nextTick(done);
diff --git a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
deleted file mode 100644
index 8fca2637326..00000000000
--- a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import Vue from 'vue';
-
-import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockMilestone } from 'spec/boards/mock_data';
-
-const createComponent = (milestone = mockMilestone) => {
- const Component = Vue.extend(IssueMilestone);
-
- return mountComponent(Component, {
- milestone,
- });
-};
-
-describe('IssueMilestoneComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isMilestoneStarted', () => {
- it('should return `false` when milestoneStart prop is not defined', done => {
- const vmStartUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStartUndefined.isMilestoneStarted).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmStartUndefined.$destroy();
- });
-
- it('should return `true` when milestone start date is past current date', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.isMilestoneStarted).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
- });
-
- describe('isMilestonePastDue', () => {
- it('should return `false` when milestoneDue prop is not defined', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.isMilestonePastDue).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('should return `true` when milestone due is past current date', done => {
- const vmPastDue = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmPastDue.isMilestonePastDue).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmPastDue.$destroy();
- });
- });
-
- describe('milestoneDatesAbsolute', () => {
- it('returns string containing absolute milestone due date', () => {
- expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
- });
-
- it('returns string containing absolute milestone start date when due date is not present', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.milestoneDatesAbsolute).toBe('(January 1, 2018)');
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('returns empty string when both milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesAbsolute).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
-
- describe('milestoneDatesHuman', () => {
- it('returns string containing milestone due date when date is yet to be due', done => {
- const vmFuture = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: `${new Date().getFullYear() + 10}-01-01`,
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmFuture.milestoneDatesHuman).toContain('years remaining');
- })
- .then(done)
- .catch(done.fail);
-
- vmFuture.$destroy();
- });
-
- it('returns string containing milestone start date when date has already started and due date is not present', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.milestoneDatesHuman).toContain('Started');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
-
- it('returns string containing milestone start date when date is yet to start and due date is not present', done => {
- const vmStarts = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: `${new Date().getFullYear() + 10}-01-01`,
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarts.milestoneDatesHuman).toContain('Starts');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarts.$destroy();
- });
-
- it('returns empty string when milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesHuman).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component root element with class `issue-milestone-details`', () => {
- expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
- });
-
- it('renders milestone icon', () => {
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
- });
-
- it('renders milestone title', () => {
- expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
- });
-
- it('renders milestone tooltip', () => {
- expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
- mockMilestone.title,
- );
- });
- });
-});
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index de406211a5b..b66acf13135 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -24,7 +24,7 @@ describe Clusters::Applications::Runner do
it 'is initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.4.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
expect(subject).to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -42,7 +42,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.4.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
end
end
end
diff --git a/spec/support/protected_branch_helpers.rb b/spec/support/protected_branch_helpers.rb
new file mode 100644
index 00000000000..ede16d1c1e2
--- /dev/null
+++ b/spec/support/protected_branch_helpers.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module ProtectedBranchHelpers
+ def set_allowed_to(operation, option = 'Maintainers', form: '.js-new-protected-branch')
+ within form do
+ select_elem = find(".js-allowed-to-#{operation}")
+ select_elem.click
+
+ wait_for_requests
+
+ within('.dropdown-content') do
+ Array(option).each { |opt| click_on(opt) }
+ end
+
+ # Enhanced select is used in EE, therefore an extra click is needed.
+ select_elem.click if select_elem['aria-expanded'] == 'true'
+ end
+ end
+
+ def set_protected_branch_name(branch_name)
+ find('.js-protected-branch-select').click
+ find('.dropdown-input-field').set(branch_name)
+ click_on("Create wildcard #{branch_name}")
+ end
+
+ def set_defaults
+ set_allowed_to('merge')
+ set_allowed_to('push')
+ end
+end
diff --git a/spec/support/protected_tag_helpers.rb b/spec/support/protected_tag_helpers.rb
new file mode 100644
index 00000000000..fe9be856286
--- /dev/null
+++ b/spec/support/protected_tag_helpers.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require_relative 'protected_branch_helpers'
+
+module ProtectedTagHelpers
+ include ::ProtectedBranchHelpers
+
+ def set_allowed_to(operation, option = 'Maintainers', form: '.new-protected-tag')
+ super
+ end
+
+ def set_protected_tag_name(tag_name)
+ find('.js-protected-tag-select').click
+ find('.dropdown-input-field').set(tag_name)
+ click_on("Create wildcard #{tag_name}")
+ find('.protected-tags-dropdown .dropdown-menu', visible: false)
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 8a23aabba20..a0446b652ac 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -663,10 +663,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.59.0.tgz#affcf9596d736836d37469bb4aea2226ac03e087"
integrity sha512-dokGyyLRRsoBKO70KP1g+ZsDGyTK/RIHWDmvWI6Bx5AxQ3UqAzVXn2OIb3owjJAexyRG1uBmJrriiVVyHznQ4g==
-"@gitlab/ui@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.4.0.tgz#174681f210eb16c3d101a36968d5e4d163c0d014"
- integrity sha512-joXNz80IHMQxEGrqcNUTEKofjfZtkOKUe34HAFI71NEeYT6H0r/lYmJ5Gcz+MmwM1CvZOVbB3DnKzxQPDbN/hQ==
+"@gitlab/ui@^3.5.0":
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.5.0.tgz#31ecfc16e3f7663545f31ddf07e02bba96a6d138"
+ integrity sha512-eDD++hhGJuH59g2QcGshuou9/NLcLfse4Abm9KOIWIaYI3NPWW2KRGwLHPB6H0d5W0/X5pyWYQvXgF7JE2ZXbA==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "^2.0.0-rc.11"
@@ -678,6 +678,7 @@
url-search-params-polyfill "^5.0.0"
vue "^2.5.21"
vue-loader "^15.4.2"
+ vue-toasted "^1.1.26"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
@@ -10982,6 +10983,11 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
+vue-toasted@^1.1.26:
+ version "1.1.26"
+ resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.26.tgz#1333d1a42157ab78389c3810023a49ba94e69c7b"
+ integrity sha512-Z4/gfPcqdzsRvif7UITrZOkh3C6jm0yQKJyr9kX31IGWXor5dNipE1Sc5SnlL5RLmY7vlLa+SqIjc9Gbpy7V0g==
+
vue-virtual-scroll-list@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76"