summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.yml5
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js3
-rw-r--r--app/assets/javascripts/registry/settings/components/registry_settings_app.vue43
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js24
-rw-r--r--app/assets/javascripts/registry/settings/stores/actions.js6
-rw-r--r--app/assets/javascripts/registry/settings/stores/index.js16
-rw-r--r--app/assets/javascripts/registry/settings/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/registry/settings/stores/mutations.js8
-rw-r--r--app/assets/javascripts/registry/settings/stores/state.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue245
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue108
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue98
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_stop_button.vue83
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue99
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue)10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue2
-rw-r--r--app/controllers/projects/releases_controller.rb1
-rw-r--r--app/models/ci/build.rb9
-rw-r--r--app/services/cohorts_service.rb2
-rw-r--r--app/views/instance_statistics/cohorts/_cohorts_table.html.haml27
-rw-r--r--app/views/projects/registry/settings/_index.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml11
-rw-r--r--changelogs/unreleased/18668-clean-up-cohorts.yml5
-rw-r--r--changelogs/unreleased/25351-texts-and-structure.yml5
-rw-r--r--changelogs/unreleased/26019-restriction-on-evidence.yml5
-rw-r--r--changelogs/unreleased/sh-reduce-gitaly-calls-in-ci-build.yml5
-rw-r--r--config/initializers/elastic_client_setup.rb22
-rw-r--r--config/initializers/zz_metrics.rb4
-rw-r--r--db/migrate/20191127151619_create_gitlab_subscription_histories.rb28
-rw-r--r--db/schema.rb17
-rw-r--r--doc/administration/raketasks/maintenance.md12
-rw-r--r--doc/ci/yaml/README.md1
-rw-r--r--doc/development/fe_guide/style/scss.md2
-rw-r--r--doc/development/prometheus_metrics.md4
-rw-r--r--doc/user/project/clusters/serverless/index.md21
-rw-r--r--doc/user/project/integrations/prometheus.md1
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/tasks/gitlab/metrics.rake8
-rw-r--r--locale/gitlab.pot45
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb20
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb5
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb36
-rw-r--r--spec/db/schema_spec.rb1
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb2
-rw-r--r--spec/frontend/issuables_list/components/issuable_spec.js1
-rw-r--r--spec/frontend/issuables_list/components/issuables_list_app_spec.js1
-rw-r--r--spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap33
-rw-r--r--spec/frontend/registry/settings/components/registry_settings_app_spec.js40
-rw-r--r--spec/frontend/registry/settings/stores/actions_spec.js20
-rw-r--r--spec/frontend/registry/settings/stores/mutations_spec.js21
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js32
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_spec.js194
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js118
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js313
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_stop_button_spec.js95
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/review_app_link_spec.js1
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js9
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js7
-rw-r--r--spec/lib/api/entities/release_spec.rb40
-rw-r--r--spec/models/ci/build_spec.rb50
-rw-r--r--spec/services/cohorts_service_spec.rb24
66 files changed, 1459 insertions, 639 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 9eba5b2ad3b..5fc4af97e2d 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -30,6 +30,11 @@ rules:
no-else-return:
- error
- allowElseIf: true
+ import/no-unresolved:
+ - error
+ - ignore:
+ # https://gitlab.com/gitlab-org/gitlab/issues/38226
+ - '^ee_component/'
import/no-useless-path-segments: off
import/order: off
lines-between-class-members: off
diff --git a/Gemfile.lock b/Gemfile.lock
index 054a491a019..cba3d118a7e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -748,7 +748,7 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.1.1)
- puma (4.3.0)
+ puma (4.3.1)
nio4r (~> 2.0)
puma_worker_killer (0.1.1)
get_process_mem (~> 0.2)
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 885247335a4..b4aac8eea2b 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -1,6 +1,7 @@
import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
+import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -32,4 +33,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
});
+
+ registrySettingsApp();
});
diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
new file mode 100644
index 00000000000..b2c700b817c
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
@@ -0,0 +1,43 @@
+<script>
+import { mapState } from 'vuex';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ components: {},
+ computed: {
+ ...mapState({
+ helpPagePath: 'helpPagePath',
+ }),
+
+ helpText() {
+ return sprintf(
+ s__(
+ 'PackageRegistry|Read more about the %{helpLinkStart}Container Registry tag retention policies%{helpLinkEnd}',
+ ),
+ {
+ helpLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
+ helpLinkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <p>
+ {{ s__('PackageRegistry|Tag retention policies are designed to:') }}
+ </p>
+ <ul>
+ <li>{{ s__('PackageRegistry|Keep and protect the images that matter most.') }}</li>
+ <li>
+ {{
+ s__("PackageRegistry|Automatically remove extra images that aren't designed to be kept.")
+ }}
+ </li>
+ </ul>
+ <p ref="help-link" v-html="helpText"></p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
new file mode 100644
index 00000000000..2938178ea86
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import store from './stores/';
+import RegistrySettingsApp from './components/registry_settings_app.vue';
+
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-registry-settings');
+ if (!el) {
+ return null;
+ }
+ store.dispatch('setInitialState', el.dataset);
+ return new Vue({
+ el,
+ store,
+ components: {
+ RegistrySettingsApp,
+ },
+ render(createElement) {
+ return createElement('registry-settings-app', {});
+ },
+ });
+};
diff --git a/app/assets/javascripts/registry/settings/stores/actions.js b/app/assets/javascripts/registry/settings/stores/actions.js
new file mode 100644
index 00000000000..f2c469d4edb
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/actions.js
@@ -0,0 +1,6 @@
+import * as types from './mutation_types';
+
+export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
+
+// to avoid eslint error until more actions are added to the store
+export default () => {};
diff --git a/app/assets/javascripts/registry/settings/stores/index.js b/app/assets/javascripts/registry/settings/stores/index.js
new file mode 100644
index 00000000000..91a35aac149
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ state,
+ actions,
+ mutations,
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/registry/settings/stores/mutation_types.js b/app/assets/javascripts/registry/settings/stores/mutation_types.js
new file mode 100644
index 00000000000..8a0f519eabd
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/mutation_types.js
@@ -0,0 +1,4 @@
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+
+// to avoid eslint error until more actions are added to the store
+export default () => {};
diff --git a/app/assets/javascripts/registry/settings/stores/mutations.js b/app/assets/javascripts/registry/settings/stores/mutations.js
new file mode 100644
index 00000000000..4f32e11ed52
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/mutations.js
@@ -0,0 +1,8 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_STATE](state, initialState) {
+ state.helpPagePath = initialState.helpPagePath;
+ state.registrySettingsEndpoint = initialState.registrySettingsEndpoint;
+ },
+};
diff --git a/app/assets/javascripts/registry/settings/stores/state.js b/app/assets/javascripts/registry/settings/stores/state.js
new file mode 100644
index 00000000000..4c0439458b6
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/state.js
@@ -0,0 +1,10 @@
+export default () => ({
+ /*
+ * Help page path to generate the link
+ */
+ helpPagePath: '',
+ /*
+ * Settings endpoint to call to fetch and update the settings
+ */
+ registrySettingsEndpoint: '',
+});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
deleted file mode 100644
index 1873e09c370..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ /dev/null
@@ -1,245 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
-import { __ } from '~/locale';
-import timeagoMixin from '../../vue_shared/mixins/timeago';
-import LoadingButton from '../../vue_shared/components/loading_button.vue';
-import { visitUrl } from '../../lib/utils/url_utility';
-import createFlash from '../../flash';
-import MemoryUsage from './memory_usage.vue';
-import StatusIcon from './mr_widget_status_icon.vue';
-import ReviewAppLink from './review_app_link.vue';
-import MRWidgetService from '../services/mr_widget_service';
-
-export default {
- // name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- name: 'Deployment',
- components: {
- LoadingButton,
- MemoryUsage,
- StatusIcon,
- Icon,
- TooltipOnTruncate,
- FilteredSearchDropdown,
- ReviewAppLink,
- VisualReviewAppLink: () =>
- import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [timeagoMixin],
- props: {
- deployment: {
- type: Object,
- required: true,
- },
- showMetrics: {
- type: Boolean,
- required: true,
- },
- showVisualReviewApp: {
- type: Boolean,
- required: false,
- default: false,
- },
- visualReviewAppMeta: {
- type: Object,
- required: false,
- default: () => ({
- sourceProjectId: '',
- sourceProjectPath: '',
- mergeRequestId: '',
- appUrl: '',
- }),
- },
- },
- deployedTextMap: {
- running: __('Deploying to'),
- success: __('Deployed to'),
- failed: __('Failed to deploy to'),
- created: __('Will deploy to'),
- canceled: __('Failed to deploy to'),
- },
- data() {
- return {
- isStopping: false,
- };
- },
- computed: {
- deployTimeago() {
- return this.timeFormated(this.deployment.deployed_at);
- },
- deploymentExternalUrl() {
- if (this.deployment.changes && this.deployment.changes.length === 1) {
- return this.deployment.changes[0].external_url;
- }
- return this.deployment.external_url;
- },
- hasExternalUrls() {
- return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
- },
- hasDeploymentTime() {
- return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
- },
- hasDeploymentMeta() {
- return Boolean(this.deployment.url && this.deployment.name);
- },
- hasMetrics() {
- return Boolean(this.deployment.metrics_url);
- },
- deployedText() {
- return this.$options.deployedTextMap[this.deployment.status];
- },
- isDeployInProgress() {
- return this.deployment.status === 'running';
- },
- deployInProgressTooltip() {
- return this.isDeployInProgress
- ? __('Stopping this environment is currently not possible as a deployment is in progress')
- : '';
- },
- shouldRenderDropdown() {
- return this.deployment.changes && this.deployment.changes.length > 1;
- },
- showMemoryUsage() {
- return this.hasMetrics && this.showMetrics;
- },
- },
- methods: {
- stopEnvironment() {
- const msg = __('Are you sure you want to stop this environment?');
- const isConfirmed = confirm(msg); // eslint-disable-line
-
- if (isConfirmed) {
- this.isStopping = true;
-
- MRWidgetService.stopEnvironment(this.deployment.stop_url)
- .then(res => res.data)
- .then(data => {
- if (data.redirect_url) {
- visitUrl(data.redirect_url);
- }
-
- this.isStopping = false;
- })
- .catch(() => {
- createFlash(
- __('Something went wrong while stopping this environment. Please try again.'),
- );
- this.isStopping = false;
- });
- }
- },
- },
-};
-</script>
-
-<template>
- <div class="deploy-heading">
- <div class="ci-widget media">
- <div class="media-body">
- <div class="deploy-body">
- <div class="js-deployment-info deployment-info">
- <template v-if="hasDeploymentMeta">
- <span> {{ deployedText }} </span>
- <tooltip-on-truncate
- :title="deployment.name"
- truncate-target="child"
- class="deploy-link label-truncate"
- >
- <a
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-meta"
- >
- {{ deployment.name }}
- </a>
- </tooltip-on-truncate>
- </template>
- <span
- v-if="hasDeploymentTime"
- v-gl-tooltip
- :title="deployment.deployed_at_formatted"
- class="js-deploy-time"
- >
- {{ deployTimeago }}
- </span>
- <memory-usage
- v-if="showMemoryUsage"
- :metrics-url="deployment.metrics_url"
- :metrics-monitoring-url="deployment.metrics_monitoring_url"
- />
- </div>
- <div>
- <template v-if="hasExternalUrls">
- <filtered-search-dropdown
- v-if="shouldRenderDropdown"
- class="js-mr-wigdet-deployment-dropdown inline"
- :items="deployment.changes"
- :main-action-link="deploymentExternalUrl"
- filter-key="path"
- >
- <template slot="mainAction" slot-scope="slotProps">
- <review-app-link
- :link="deploymentExternalUrl"
- :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
- />
- </template>
-
- <template slot="result" slot-scope="slotProps">
- <a
- :href="slotProps.result.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="menu-item"
- >
- <strong class="str-truncated-100 append-bottom-0 d-block">
- {{ slotProps.result.path }}
- </strong>
-
- <p class="text-secondary str-truncated-100 append-bottom-0 d-block">
- {{ slotProps.result.external_url }}
- </p>
- </a>
- </template>
- </filtered-search-dropdown>
- <template v-else>
- <review-app-link
- :link="deploymentExternalUrl"
- css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
- />
- </template>
- <visual-review-app-link
- v-if="showVisualReviewApp"
- :link="deploymentExternalUrl"
- :app-metadata="visualReviewAppMeta"
- />
- </template>
- <span
- v-if="deployment.stop_url"
- v-gl-tooltip
- :title="deployInProgressTooltip"
- class="d-inline-block"
- tabindex="0"
- >
- <loading-button
- :loading="isStopping"
- :disabled="isDeployInProgress"
- :title="__('Stop environment')"
- container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
- @click="stopEnvironment"
- >
- <icon name="stop" />
- </loading-button>
- </span>
- </div>
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
new file mode 100644
index 00000000000..90741e3aa44
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
@@ -0,0 +1,8 @@
+// DEPLOYMENT STATUSES
+export const CREATED = 'created';
+export const MANUAL_DEPLOY = 'manual_deploy';
+export const WILL_DEPLOY = 'will_deploy';
+export const RUNNING = 'running';
+export const SUCCESS = 'success';
+export const FAILED = 'failed';
+export const CANCELED = 'canceled';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
new file mode 100644
index 00000000000..e03b1e6d6a6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
@@ -0,0 +1,108 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import DeploymentInfo from './deployment_info.vue';
+import DeploymentViewButton from './deployment_view_button.vue';
+import DeploymentStopButton from './deployment_stop_button.vue';
+import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED, RUNNING, SUCCESS } from './constants';
+
+export default {
+ // name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ name: 'Deployment',
+ components: {
+ DeploymentInfo,
+ DeploymentStopButton,
+ DeploymentViewButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ deployment: {
+ type: Object,
+ required: true,
+ },
+ showMetrics: {
+ type: Boolean,
+ required: true,
+ },
+ showVisualReviewApp: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visualReviewAppMeta: {
+ type: Object,
+ required: false,
+ default: () => ({
+ sourceProjectId: '',
+ sourceProjectPath: '',
+ mergeRequestId: '',
+ appUrl: '',
+ }),
+ },
+ },
+ computed: {
+ canBeManuallyDeployed() {
+ return this.computedDeploymentStatus === MANUAL_DEPLOY;
+ },
+ computedDeploymentStatus() {
+ if (this.deployment.status === CREATED) {
+ return this.isManual ? MANUAL_DEPLOY : WILL_DEPLOY;
+ }
+ return this.deployment.status;
+ },
+ hasExternalUrls() {
+ return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
+ },
+ hasPreviousDeployment() {
+ return Boolean(!this.isCurrent && this.deployment.deployed_at);
+ },
+ isCurrent() {
+ return this.computedDeploymentStatus === SUCCESS;
+ },
+ isManual() {
+ return Boolean(
+ this.deployment.details &&
+ this.deployment.details.playable_build &&
+ this.deployment.details.playable_build.play_path,
+ );
+ },
+ isDeployInProgress() {
+ return this.deployment.status === RUNNING;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="deploy-heading">
+ <div class="ci-widget media">
+ <div class="media-body">
+ <div class="deploy-body">
+ <deployment-info
+ :computed-deployment-status="computedDeploymentStatus"
+ :deployment="deployment"
+ :show-metrics="showMetrics"
+ />
+ <div>
+ <!-- show appropriate version of review app button -->
+ <deployment-view-button
+ v-if="hasExternalUrls"
+ :is-current="isCurrent"
+ :deployment="deployment"
+ :show-visual-review-app="showVisualReviewApp"
+ :visual-review-app-metadata="visualReviewAppMeta"
+ />
+ <!-- if it is stoppable, show stop -->
+ <deployment-stop-button
+ v-if="deployment.stop_url"
+ :is-deploy-in-progress="isDeployInProgress"
+ :stop-url="deployment.stop_url"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
new file mode 100644
index 00000000000..47e70e972cf
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import MemoryUsage from './memory_usage.vue';
+import { MANUAL_DEPLOY, WILL_DEPLOY, RUNNING, SUCCESS, FAILED, CANCELED } from './constants';
+
+export default {
+ name: 'DeploymentInfo',
+ components: {
+ GlLink,
+ MemoryUsage,
+ TooltipOnTruncate,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ computedDeploymentStatus: {
+ type: String,
+ required: true,
+ },
+ deployment: {
+ type: Object,
+ required: true,
+ },
+ showMetrics: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ deployedTextMap: {
+ [MANUAL_DEPLOY]: __('Can deploy manually to'),
+ [WILL_DEPLOY]: __('Will deploy to'),
+ [RUNNING]: __('Deploying to'),
+ [SUCCESS]: __('Deployed to'),
+ [FAILED]: __('Failed to deploy to'),
+ [CANCELED]: __('Canceled deploy to'),
+ },
+ computed: {
+ deployTimeago() {
+ return this.timeFormated(this.deployment.deployed_at);
+ },
+ deployedText() {
+ return this.$options.deployedTextMap[this.computedDeploymentStatus];
+ },
+ hasDeploymentTime() {
+ return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
+ },
+ hasDeploymentMeta() {
+ return Boolean(this.deployment.url && this.deployment.name);
+ },
+ hasMetrics() {
+ return Boolean(this.deployment.metrics_url);
+ },
+ showMemoryUsage() {
+ return this.hasMetrics && this.showMetrics;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="js-deployment-info deployment-info">
+ <template v-if="hasDeploymentMeta">
+ <span>{{ deployedText }}</span>
+ <tooltip-on-truncate
+ :title="deployment.name"
+ truncate-target="child"
+ class="deploy-link label-truncate"
+ >
+ <gl-link
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-meta gl-font-size-12"
+ >
+ {{ deployment.name }}
+ </gl-link>
+ </tooltip-on-truncate>
+ </template>
+ <span
+ v-if="hasDeploymentTime"
+ v-gl-tooltip
+ :title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
+ >
+ {{ deployTimeago }}
+ </span>
+ <memory-usage
+ v-if="showMemoryUsage"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_stop_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_stop_button.vue
new file mode 100644
index 00000000000..29e38648573
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_stop_button.vue
@@ -0,0 +1,83 @@
+<script>
+import { __ } from '~/locale';
+import { GlTooltipDirective } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
+import MRWidgetService from '../../services/mr_widget_service';
+
+export default {
+ name: 'DeploymentStopButton',
+ components: {
+ LoadingButton,
+ Icon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ isDeployInProgress: {
+ type: Boolean,
+ required: true,
+ },
+ stopUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isStopping: false,
+ };
+ },
+ computed: {
+ deployInProgressTooltip() {
+ return this.isDeployInProgress
+ ? __('Stopping this environment is currently not possible as a deployment is in progress')
+ : '';
+ },
+ },
+ methods: {
+ stopEnvironment() {
+ const msg = __('Are you sure you want to stop this environment?');
+ const isConfirmed = confirm(msg); // eslint-disable-line
+
+ if (isConfirmed) {
+ this.isStopping = true;
+
+ MRWidgetService.stopEnvironment(this.stopUrl)
+ .then(res => res.data)
+ .then(data => {
+ if (data.redirect_url) {
+ visitUrl(data.redirect_url);
+ }
+
+ this.isStopping = false;
+ })
+ .catch(() => {
+ createFlash(
+ __('Something went wrong while stopping this environment. Please try again.'),
+ );
+ this.isStopping = false;
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <span v-gl-tooltip :title="deployInProgressTooltip" class="d-inline-block" tabindex="0">
+ <loading-button
+ v-gl-tooltip
+ :loading="isStopping"
+ :disabled="isDeployInProgress"
+ :title="__('Stop environment')"
+ container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
+ @click="stopEnvironment"
+ >
+ <icon name="stop" />
+ </loading-button>
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
new file mode 100644
index 00000000000..9965e3d5203
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -0,0 +1,99 @@
+<script>
+import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
+import ReviewAppLink from '../review_app_link.vue';
+
+export default {
+ name: 'DeploymentViewButton',
+ components: {
+ FilteredSearchDropdown,
+ ReviewAppLink,
+ VisualReviewAppLink: () =>
+ import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
+ },
+ props: {
+ deployment: {
+ type: Object,
+ required: true,
+ },
+ isCurrent: {
+ type: Boolean,
+ required: true,
+ },
+ showVisualReviewApp: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visualReviewAppMeta: {
+ type: Object,
+ required: false,
+ default: () => ({
+ sourceProjectId: '',
+ sourceProjectPath: '',
+ mergeRequestId: '',
+ appUrl: '',
+ }),
+ },
+ },
+ computed: {
+ deploymentExternalUrl() {
+ if (this.deployment.changes && this.deployment.changes.length === 1) {
+ return this.deployment.changes[0].external_url;
+ }
+ return this.deployment.external_url;
+ },
+ shouldRenderDropdown() {
+ return this.deployment.changes && this.deployment.changes.length > 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <filtered-search-dropdown
+ v-if="shouldRenderDropdown"
+ class="js-mr-wigdet-deployment-dropdown inline"
+ :items="deployment.changes"
+ :main-action-link="deploymentExternalUrl"
+ filter-key="path"
+ >
+ <template slot="mainAction" slot-scope="slotProps">
+ <review-app-link
+ :is-current="isCurrent"
+ :link="deploymentExternalUrl"
+ :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
+ />
+ </template>
+
+ <template slot="result" slot-scope="slotProps">
+ <a
+ :href="slotProps.result.external_url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-url-menu-item menu-item"
+ >
+ <strong class="str-truncated-100 append-bottom-0 d-block">
+ {{ slotProps.result.path }}
+ </strong>
+
+ <p class="text-secondary str-truncated-100 append-bottom-0 d-block">
+ {{ slotProps.result.external_url }}
+ </p>
+ </a>
+ </template>
+ </filtered-search-dropdown>
+ <template v-else>
+ <review-app-link
+ :is-current="isCurrent"
+ :link="deploymentExternalUrl"
+ css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
+ />
+ </template>
+ <visual-review-app-link
+ v-if="showVisualReviewApp"
+ :link="deploymentExternalUrl"
+ :app-metadata="visualReviewAppMeta"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index 7113b72f8c5..fe41a15979e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -1,10 +1,10 @@
<script>
import { sprintf, s__ } from '~/locale';
-import statusCodes from '../../lib/utils/http_status';
-import { bytesToMiB } from '../../lib/utils/number_utils';
-import { backOff } from '../../lib/utils/common_utils';
-import MemoryGraph from '../../vue_shared/components/memory_graph.vue';
-import MRWidgetService from '../services/mr_widget_service';
+import statusCodes from '~/lib/utils/http_status';
+import { bytesToMiB } from '~/lib/utils/number_utils';
+import { backOff } from '~/lib/utils/common_utils';
+import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
+import MRWidgetService from '../../services/mr_widget_service';
export default {
name: 'MemoryUsage',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index a297156ab10..c8b26889076 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -1,7 +1,7 @@
<script>
import _ from 'underscore';
import ArtifactsApp from './artifacts_list_app.vue';
-import Deployment from './deployment.vue';
+import Deployment from './deployment/deployment.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
index d2d32492e6c..1550ec0f21e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -6,15 +7,24 @@ export default {
Icon,
},
props: {
- link: {
+ cssClass: {
type: String,
required: true,
},
- cssClass: {
+ isCurrent: {
+ type: Boolean,
+ required: true,
+ },
+ link: {
type: String,
required: true,
},
},
+ computed: {
+ linkText() {
+ return this.isCurrent ? __('View app') : __('View previous app');
+ },
+ },
};
</script>
<template>
@@ -26,6 +36,6 @@ export default {
data-track-event="open_review_app"
data-track-label="review_app"
>
- {{ __('View app') }} <icon class="fgray" name="external-link" />
+ {{ linkText }} <icon class="fgray" name="external-link" />
</a>
</template>
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 edd21a81f8b..363fe226f15 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
@@ -10,7 +10,7 @@ import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
-import Deployment from './components/deployment.vue';
+import Deployment from './components/deployment/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import MergedState from './components/states/mr_widget_merged.vue';
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index a9a264183b3..ffe69fe97e4 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -10,6 +10,7 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_evidence_collection, project)
end
before_action :authorize_update_release!, only: %i[edit update]
+ before_action :authorize_download_code!, only: [:evidence]
def index
respond_to do |format|
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1293b0d0f59..b8068a22c02 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -662,9 +662,8 @@ module Ci
def execute_hooks
return unless project
- build_data = Gitlab::DataBuilder::Build.build(self)
- project.execute_hooks(build_data.dup, :job_hooks)
- project.execute_services(build_data.dup, :job_hooks)
+ project.execute_hooks(build_data.dup, :job_hooks) if project.has_active_hooks?(:job_hooks)
+ project.execute_services(build_data.dup, :job_hooks) if project.has_active_services?(:job_hooks)
end
def browsable_artifacts?
@@ -873,6 +872,10 @@ module Ci
private
+ def build_data
+ @build_data ||= Gitlab::DataBuilder::Build.build(self)
+ end
+
def successful_deployment_status
if deployment&.last?
:last
diff --git a/app/services/cohorts_service.rb b/app/services/cohorts_service.rb
index dbbe89ef260..03be87f4cc1 100644
--- a/app/services/cohorts_service.rb
+++ b/app/services/cohorts_service.rb
@@ -38,7 +38,7 @@ class CohortsService
{
registration_month: registration_month,
- activity_months: activity_months,
+ activity_months: activity_months[1..-1],
total: activity_months.first[:total],
inactive: inactive
}
diff --git a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml b/app/views/instance_statistics/cohorts/_cohorts_table.html.haml
index 6a7c999bff3..d4defd3f849 100644
--- a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml
+++ b/app/views/instance_statistics/cohorts/_cohorts_table.html.haml
@@ -1,25 +1,32 @@
+- number_of_data_columns = @cohorts[:months_included] - 1
.bs-callout.clearfix
%p
- User cohorts are shown for the last #{@cohorts[:months_included]}
- months. Only users with activity are counted in the cohort total; inactive
- users are counted separately.
+ = s_("Cohorts|User cohorts are shown for the last %{months_included} months. Only users with activity are counted in the 'New users' column; inactive users are counted separately.") % { months_included: @cohorts[:months_included] }
= link_to icon('question-circle'), help_page_path('user/instance_statistics/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
-.table-holder
+.table-holder.d-xl-table
%table.table
%thead
%tr
- %th Registration month
- %th Inactive users
- %th Cohort total
- - @cohorts[:months_included].times do |i|
- %th Month #{i}
+ %th.border-right.pt-4{ colspan: 3 }
+ %th.font-weight-bold.pt-4{ colspan: number_of_data_columns }
+ = s_("Cohorts|Returning users")
+ %tr
+ %th.border-top-0
+ = s_("Cohorts|Registration month")
+ %th.border-top-0
+ = s_("Cohorts|Inactive users")
+ %th.border-top-0.border-right
+ = s_("Cohorts|New users")
+ - number_of_data_columns.times do |i|
+ %th.border-top-0
+ = s_("Cohorts|Month %{month_index}") % { month_index: i + 1 }
%tbody
- @cohorts[:cohorts].each do |cohort|
%tr
%td= cohort[:registration_month]
%td= cohort[:inactive]
- %td= cohort[:total]
+ %td.border-right= cohort[:total]
- cohort[:activity_months].each do |activity_month|
%td
- next if cohort[:total] == '0'
diff --git a/app/views/projects/registry/settings/_index.haml b/app/views/projects/registry/settings/_index.haml
new file mode 100644
index 00000000000..e1eed93664e
--- /dev/null
+++ b/app/views/projects/registry/settings/_index.haml
@@ -0,0 +1,2 @@
+#js-registry-settings{ data: { registry_settings_endpoint: '',
+ help_page_path: help_page_path('user/project/operations/linking_to_an_external_dashboard') } }
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index d2265da26c3..38483f599b7 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -59,3 +59,14 @@
.settings-content
= render 'projects/triggers/index'
+- if Feature.enabled?(:registry_retention_policies_settings, @project)
+ %section.settings.no-animate#js-registry-polcies{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _("Container Registry tag expiration policies")
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _("Expiration policies for the Container Registry are a perfect solution for keeping the Registry space down while still enjoying the full power of GitLab CI/CD.")
+ .settings-content
+ = render 'projects/registry/settings/index'
diff --git a/changelogs/unreleased/18668-clean-up-cohorts.yml b/changelogs/unreleased/18668-clean-up-cohorts.yml
new file mode 100644
index 00000000000..3cb8b304018
--- /dev/null
+++ b/changelogs/unreleased/18668-clean-up-cohorts.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up the cohorts table
+merge_request: 20779
+author:
+type: changed
diff --git a/changelogs/unreleased/25351-texts-and-structure.yml b/changelogs/unreleased/25351-texts-and-structure.yml
new file mode 100644
index 00000000000..aa7c7205e32
--- /dev/null
+++ b/changelogs/unreleased/25351-texts-and-structure.yml
@@ -0,0 +1,5 @@
+---
+title: Update information and button text for deployment footer
+merge_request: 18918
+author:
+type: changed
diff --git a/changelogs/unreleased/26019-restriction-on-evidence.yml b/changelogs/unreleased/26019-restriction-on-evidence.yml
new file mode 100644
index 00000000000..21157eeecd3
--- /dev/null
+++ b/changelogs/unreleased/26019-restriction-on-evidence.yml
@@ -0,0 +1,5 @@
+---
+title: Evidence - Added restriction for guest on Release page
+merge_request: 21102
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-reduce-gitaly-calls-in-ci-build.yml b/changelogs/unreleased/sh-reduce-gitaly-calls-in-ci-build.yml
new file mode 100644
index 00000000000..a9a940da48f
--- /dev/null
+++ b/changelogs/unreleased/sh-reduce-gitaly-calls-in-ci-build.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce Gitaly calls in BuildHooksWorker
+merge_request: 20365
+author:
+type: performance
diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb
index a1abb29838b..f38b606b3a8 100644
--- a/config/initializers/elastic_client_setup.rb
+++ b/config/initializers/elastic_client_setup.rb
@@ -7,6 +7,17 @@ require 'gitlab/current_settings'
Gitlab.ee do
require 'elasticsearch/model'
+ ### Monkey patches
+
+ Elasticsearch::Model::Response::Records.prepend GemExtensions::Elasticsearch::Model::Response::Records
+ Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records
+ Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods
+ Elasticsearch::Model::Adapter::ActiveRecord::Importing.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Importing
+ Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client
+ Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
+ Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
+ Elasticsearch::Model.singleton_class.prepend GemExtensions::Elasticsearch::Model::Client
+
### Modified from elasticsearch-model/lib/elasticsearch/model.rb
[
@@ -32,15 +43,4 @@ Gitlab.ee do
target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super
end
CODE
-
- ### Monkey patches
-
- Elasticsearch::Model::Response::Records.prepend GemExtensions::Elasticsearch::Model::Response::Records
- Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records
- Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods
- Elasticsearch::Model::Adapter::ActiveRecord::Importing.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Importing
- Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client
- Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
- Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
- Elasticsearch::Model.singleton_class.prepend GemExtensions::Elasticsearch::Model::Client
end
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index bc28780cc77..416611ef46c 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -88,8 +88,8 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Gitlab::Highlight)
Gitlab.ee do
- instrumentation.instrument_methods(Elasticsearch::Git::Repository)
- instrumentation.instrument_instance_methods(Elasticsearch::Git::Repository)
+ instrumentation.instrument_instance_methods(Elastic::Latest::GitInstanceProxy)
+ instrumentation.instrument_instance_methods(Elastic::Latest::GitClassProxy)
instrumentation.instrument_instance_methods(Search::GlobalService)
instrumentation.instrument_instance_methods(Search::ProjectService)
diff --git a/db/migrate/20191127151619_create_gitlab_subscription_histories.rb b/db/migrate/20191127151619_create_gitlab_subscription_histories.rb
new file mode 100644
index 00000000000..718f2c1b313
--- /dev/null
+++ b/db/migrate/20191127151619_create_gitlab_subscription_histories.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class CreateGitlabSubscriptionHistories < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ create_table :gitlab_subscription_histories do |t|
+ t.datetime_with_timezone :gitlab_subscription_created_at
+ t.datetime_with_timezone :gitlab_subscription_updated_at
+ t.date :start_date
+ t.date :end_date
+ t.date :trial_ends_on
+ t.integer :namespace_id, null: true
+ t.integer :hosted_plan_id, null: true
+ t.integer :max_seats_used
+ t.integer :seats
+ t.boolean :trial
+ t.integer :change_type, limit: 2
+ t.bigint :gitlab_subscription_id, null: false
+ t.datetime_with_timezone :created_at
+ end
+ add_index :gitlab_subscription_histories, :gitlab_subscription_id
+ end
+
+ def down
+ drop_table :gitlab_subscription_histories
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c33223f8b59..c6fa84e96b5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1828,6 +1828,23 @@ ActiveRecord::Schema.define(version: 2019_12_02_031812) do
t.index ["upload_id"], name: "index_geo_upload_deleted_events_on_upload_id"
end
+ create_table "gitlab_subscription_histories", force: :cascade do |t|
+ t.datetime_with_timezone "gitlab_subscription_created_at"
+ t.datetime_with_timezone "gitlab_subscription_updated_at"
+ t.date "start_date"
+ t.date "end_date"
+ t.date "trial_ends_on"
+ t.integer "namespace_id"
+ t.integer "hosted_plan_id"
+ t.integer "max_seats_used"
+ t.integer "seats"
+ t.boolean "trial"
+ t.integer "change_type", limit: 2
+ t.bigint "gitlab_subscription_id", null: false
+ t.datetime_with_timezone "created_at"
+ t.index ["gitlab_subscription_id"], name: "index_gitlab_subscription_histories_on_gitlab_subscription_id"
+ end
+
create_table "gitlab_subscriptions", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index e63e0c40393..149b8d62309 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -270,3 +270,15 @@ database: gitlabhq_production
--------------------------------------------------
up migration_id migration_name
```
+
+## Import common metrics
+
+Sometimes you may need to re-import the common metrics that power the Metrics dashboards.
+
+This could be as a result of [updating existing metrics](../../development/prometheus_metrics.md#update-existing-metrics), or as a [troubleshooting measure](../../user/project/integrations/prometheus.md#troubleshooting).
+
+To re-import the metrics you can run:
+
+```sh
+sudo gitlab-rake metrics:setup_common_metrics
+```
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4a06f99f99e..714de3bac36 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -75,6 +75,7 @@ cannot be used as job names**:
- `after_script`
- `variables`
- `cache`
+- `include`
### Using reserved keywords
diff --git a/doc/development/fe_guide/style/scss.md b/doc/development/fe_guide/style/scss.md
index 02e7351d135..a8755e634be 100644
--- a/doc/development/fe_guide/style/scss.md
+++ b/doc/development/fe_guide/style/scss.md
@@ -249,7 +249,7 @@ scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to
catch any warnings.
If the Rake task is throwing warnings you don't understand, SCSS Lint's
-documentation includes [a full list of their linters][scss-lint-documentation](https://github.com/sds/scss-lint/blob/master/lib/scss_lint/linter/README.md).
+documentation includes [a full list of their linters](https://github.com/sds/scss-lint/blob/master/lib/scss_lint/linter/README.md).
### Fixing issues
diff --git a/doc/development/prometheus_metrics.md b/doc/development/prometheus_metrics.md
index b479c053862..74dbcddc5db 100644
--- a/doc/development/prometheus_metrics.md
+++ b/doc/development/prometheus_metrics.md
@@ -22,7 +22,9 @@ The requirement for adding a new metric is to make each query to have an unique
### Update existing metrics
-After you add or change existing _common_ metric you have to create a new database migration that will query and update all existing metrics.
+After you add or change an existing common metric, you must [re-run the import script](../administration/raketasks/maintenance.md#import-common-metrics) that will query and update all existing metrics.
+
+Or, you can create a database migration:
NOTE: **Note:**
If a query metric (which is identified by `id:`) is removed it will not be removed from database by default.
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index e827d848cce..529c3d9d141 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -348,7 +348,7 @@ project):
echo-js:
handler: echo-js
source: ./echo-js
- runtime: https://gitlab.com/gitlab-org/serverless/runtimes/nodejs
+ runtime: gitlab/runtimes/nodejs
description: "node.js runtime function"
environment:
MY_FUNCTION: echo-js
@@ -379,10 +379,27 @@ subsequent lines contain the function attributes.
|-----------|-------------|
| `handler` | The function's name. |
| `source` | Directory with sources of a functions. |
-| `runtime` (optional)| The runtime to be used to execute the function. When the runtime is not specified, we assume that `Dockerfile` is present in the function directory specified by `source`. |
+| `runtime` (optional)| The runtime to be used to execute the function. This can be a runtime alias (see [Runtime aliases](#runtime-aliases)), or it can be a full URL to a custom runtime repository. When the runtime is not specified, we assume that `Dockerfile` is present in the function directory specified by `source`. |
| `description` | A short description of the function. |
| `environment` | Sets an environment variable for the specific function only. |
+#### Runtime aliases
+
+The optional `runtime` parameter can refer to one of the following runtime aliases (also see [Supported runtimes](#supported-runtimes)):
+
+| Runtime alias | Maintained by |
+|-------------|---------------|
+| `gitlab/runtimes/go` | GitLab |
+| `gitlab/runtimes/nodejs` | GitLab |
+| `gitlab/runtimes/ruby` | GitLab |
+| `openfaas/classic/csharp` | OpenFaaS |
+| `openfaas/classic/go` | OpenFaaS |
+| `openfaas/classic/node` | OpenFaaS |
+| `openfaas/classic/php7` | OpenFaaS |
+| `openfaas/classic/python` | OpenFaaS |
+| `openfaas/classic/python3` | OpenFaaS |
+| `openfaas/classic/ruby` | OpenFaaS |
+
After the `gitlab-ci.yml` template has been added and the `serverless.yml` file
has been created, pushing a commit to your project will result in a CI pipeline
being executed which will deploy each function as a Knative service. Once the
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 25ba0a47a4e..de8aea6e468 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -574,6 +574,7 @@ If the "No data found" screen continues to appear, it could be due to:
are not labeled correctly. To test this, connect to the Prometheus server and
[run a query](prometheus_library/kubernetes.html#metrics-supported), replacing `$CI_ENVIRONMENT_SLUG`
with the name of your environment.
+- You may need to re-add the GitLab predefined common metrics. This can be done by running the [import common metrics rake task](../../../administration/raketasks/maintenance.md#import-common-metrics).
[autodeploy]: ../../../topics/autodevops/index.md#auto-deploy
[kubernetes]: https://kubernetes.io
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index d16786e47b2..1e41e28c449 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1319,7 +1319,7 @@ module API
expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? }
expose :commit_path, expose_nil: false
expose :tag_path, expose_nil: false
- expose :evidence_sha, expose_nil: false
+ expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :assets do
expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources]
@@ -1329,7 +1329,7 @@ module API
expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted
end
- expose :evidence_file_path, expose_nil: false
+ expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? }
end
expose :_links do
expose :merge_requests_url, expose_nil: false
diff --git a/lib/tasks/gitlab/metrics.rake b/lib/tasks/gitlab/metrics.rake
new file mode 100644
index 00000000000..8a57e400dbe
--- /dev/null
+++ b/lib/tasks/gitlab/metrics.rake
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+namespace :metrics do
+ desc "GitLab | Setup common metrics"
+ task setup_common_metrics: :gitlab_environment do
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 41f0902147d..d556c699bd3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2942,6 +2942,9 @@ msgstr ""
msgid "Callback URL"
msgstr ""
+msgid "Can deploy manually to"
+msgstr ""
+
msgid "Can override approvers and approvals required per merge request"
msgstr ""
@@ -2969,6 +2972,9 @@ msgstr ""
msgid "Cancel this job"
msgstr ""
+msgid "Canceled deploy to"
+msgstr ""
+
msgid "Cancelling Preview"
msgstr ""
@@ -4334,6 +4340,24 @@ msgstr ""
msgid "Cohorts"
msgstr ""
+msgid "Cohorts|Inactive users"
+msgstr ""
+
+msgid "Cohorts|Month %{month_index}"
+msgstr ""
+
+msgid "Cohorts|New users"
+msgstr ""
+
+msgid "Cohorts|Registration month"
+msgstr ""
+
+msgid "Cohorts|Returning users"
+msgstr ""
+
+msgid "Cohorts|User cohorts are shown for the last %{months_included} months. Only users with activity are counted in the 'New users' column; inactive users are counted separately."
+msgstr ""
+
msgid "Collapse"
msgstr ""
@@ -4615,6 +4639,9 @@ msgstr ""
msgid "Container Registry"
msgstr ""
+msgid "Container Registry tag expiration policies"
+msgstr ""
+
msgid "Container Scanning"
msgstr ""
@@ -7092,6 +7119,9 @@ msgstr ""
msgid "Expiration date"
msgstr ""
+msgid "Expiration policies for the Container Registry are a perfect solution for keeping the Registry space down while still enjoying the full power of GitLab CI/CD."
+msgstr ""
+
msgid "Expired"
msgstr ""
@@ -12141,6 +12171,9 @@ msgstr ""
msgid "Package was removed"
msgstr ""
+msgid "PackageRegistry|Automatically remove extra images that aren't designed to be kept."
+msgstr ""
+
msgid "PackageRegistry|Copy Maven XML"
msgstr ""
@@ -12180,6 +12213,9 @@ msgstr ""
msgid "PackageRegistry|Installation"
msgstr ""
+msgid "PackageRegistry|Keep and protect the images that matter most."
+msgstr ""
+
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
@@ -12192,12 +12228,18 @@ msgstr ""
msgid "PackageRegistry|Package installation"
msgstr ""
+msgid "PackageRegistry|Read more about the %{helpLinkStart}Container Registry tag retention policies%{helpLinkEnd}"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
msgid "PackageRegistry|Remove package"
msgstr ""
+msgid "PackageRegistry|Tag retention policies are designed to:"
+msgstr ""
+
msgid "PackageRegistry|There are no packages yet"
msgstr ""
@@ -19533,6 +19575,9 @@ msgstr ""
msgid "View open merge request"
msgstr ""
+msgid "View previous app"
+msgstr ""
+
msgid "View project labels"
msgstr ""
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 349d73f13ca..ac13c0f2d9e 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -154,7 +154,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
.and_return(merge_request)
end
- it 'does not serialize builds in exposed stages', :sidekiq_might_not_need_inline do
+ it 'does not serialize builds in exposed stages' do
get_show_json
json_response.dig('pipeline', 'details', 'stages').tap do |stages|
@@ -183,7 +183,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'job is cancelable' do
let(:job) { create(:ci_build, :running, pipeline: pipeline) }
- it 'cancel_path is present with correct redirect', :sidekiq_might_not_need_inline do
+ it 'cancel_path is present with correct redirect' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path']))
@@ -193,7 +193,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'with web terminal' do
let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }
- it 'exposes the terminal path', :sidekiq_might_not_need_inline do
+ it 'exposes the terminal path' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['terminal_path']).to match(%r{/terminal})
@@ -268,7 +268,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
project.add_maintainer(user) # Need to be a maintianer to view cluster.path
end
- it 'exposes the deployment information', :sidekiq_might_not_need_inline do
+ it 'exposes the deployment information' do
get_show_json
expect(response).to have_gitlab_http_status(:ok)
@@ -292,7 +292,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user)
end
- it 'user can edit runner', :sidekiq_might_not_need_inline do
+ it 'user can edit runner' do
get_show_json
expect(response).to have_gitlab_http_status(:ok)
@@ -312,7 +312,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user)
end
- it 'user can not edit runner', :sidekiq_might_not_need_inline do
+ it 'user can not edit runner' do
get_show_json
expect(response).to have_gitlab_http_status(:ok)
@@ -331,7 +331,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user)
end
- it 'user can not edit runner', :sidekiq_might_not_need_inline do
+ it 'user can not edit runner' do
get_show_json
expect(response).to have_gitlab_http_status(:ok)
@@ -412,7 +412,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'when job has trace' do
let(:job) { create(:ci_build, :running, :trace_live, pipeline: pipeline) }
- it "has_trace is true", :sidekiq_might_not_need_inline do
+ it "has_trace is true" do
get_show_json
expect(response).to match_response_schema('job/job_details')
@@ -458,7 +458,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
end
- context 'user is a maintainer', :sidekiq_might_not_need_inline do
+ context 'user is a maintainer' do
before do
project.add_maintainer(user)
@@ -512,7 +512,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
def get_show_json
expect { get_show(id: job.id, format: :json) }
- .not_to change { Gitlab::GitalyClient.get_request_count }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(1) # ListCommitsByOid
end
def get_show(**extra_params)
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 3c7f69f0e6e..b4549e4e635 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -93,7 +93,7 @@ describe Projects::PipelinesController do
end
context 'when performing gitaly calls', :request_store do
- it 'limits the Gitaly requests', :sidekiq_might_not_need_inline do
+ it 'limits the Gitaly requests' do
# Isolate from test preparation (Repository#exists? is also cached in RequestStore)
RequestStore.end!
RequestStore.clear!
@@ -101,8 +101,9 @@ describe Projects::PipelinesController do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+ # ListCommitsByOid, RepositoryExists, HasLocalBranches
expect { get_pipelines_index_json }
- .to change { Gitlab::GitalyClient.get_request_count }.by(2)
+ .to change { Gitlab::GitalyClient.get_request_count }.by(3)
end
end
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index 5b528c3be52..6592d8d5702 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -184,19 +184,39 @@ describe Projects::ReleasesController do
sign_in(user)
end
- it 'returns the correct evidence summary as a json' do
- subject
+ context 'when the user is a developer' do
+ it 'returns the correct evidence summary as a json' do
+ subject
+
+ expect(json_response).to eq(release.evidence.summary)
+ end
- expect(json_response).to eq(release.evidence.summary)
+ context 'when the release was created before evidence existed' do
+ before do
+ release.evidence.destroy
+ end
+
+ it 'returns an empty json' do
+ subject
+
+ expect(json_response).to eq({})
+ end
+ end
end
- context 'when the release was created before evidence existed' do
- it 'returns an empty json' do
- release.evidence.destroy
+ context 'when the user is a guest for the project' do
+ before do
+ project.add_guest(user)
+ end
- subject
+ context 'when the project is private' do
+ let(:project) { private_project }
+
+ it_behaves_like 'not found'
+ end
- expect(json_response).to eq({})
+ context 'when the project is public' do
+ it_behaves_like 'successful request'
end
end
end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index d4fab48b426..a42916a83a6 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -43,6 +43,7 @@ describe 'Database schema' do
geo_nodes: %w[oauth_application_id],
geo_repository_deleted_events: %w[project_id],
geo_upload_deleted_events: %w[upload_id model_id],
+ gitlab_subscription_histories: %w[gitlab_subscription_id hosted_plan_id namespace_id],
import_failures: %w[project_id],
identities: %w[user_id],
issues: %w[last_edited_by_id state_id],
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index cdffd2ae2f6..3743ef0f25d 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -96,7 +96,7 @@ describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page).to have_content("Failed to deploy to #{environment.name}")
+ expect(page).to have_content("Canceled deploy to #{environment.name}")
expect(page).not_to have_css('.js-deploy-time')
end
end
diff --git a/spec/frontend/issuables_list/components/issuable_spec.js b/spec/frontend/issuables_list/components/issuable_spec.js
index 6148f3c68f2..b6851a0e24c 100644
--- a/spec/frontend/issuables_list/components/issuable_spec.js
+++ b/spec/frontend/issuables_list/components/issuable_spec.js
@@ -45,6 +45,7 @@ describe('Issuable component', () => {
...props,
},
sync: false,
+ attachToDocument: true,
});
};
diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
index e598a9c5a5d..dad4e74cb40 100644
--- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
@@ -49,6 +49,7 @@ describe('Issuables list component', () => {
},
localVue,
sync: false,
+ attachToDocument: true,
});
};
diff --git a/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap
new file mode 100644
index 00000000000..c6dbb1da8e9
--- /dev/null
+++ b/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry List renders 1`] = `
+<div>
+ <p>
+
+ Tag retention policies are designed to:
+
+ </p>
+
+ <ul>
+ <li>
+ Keep and protect the images that matter most.
+ </li>
+
+ <li>
+
+ Automatically remove extra images that aren't designed to be kept.
+
+ </li>
+ </ul>
+
+ <p>
+ Read more about the
+ <a
+ href="foo"
+ target="_blank"
+ >
+ Container Registry tag retention policies
+ </a>
+ </p>
+</div>
+`;
diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js
new file mode 100644
index 00000000000..666d970aa6b
--- /dev/null
+++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js
@@ -0,0 +1,40 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import component from '~/registry/settings/components/registry_settings_app.vue';
+import { createStore } from '~/registry/settings/stores/';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Registry List', () => {
+ let wrapper;
+ let store;
+
+ const helpPagePath = 'foo';
+ const findHelpLink = () => wrapper.find({ ref: 'help-link' }).find('a');
+
+ const mountComponent = (options = {}) =>
+ shallowMount(component, {
+ sync: false,
+ store,
+ ...options,
+ });
+
+ beforeEach(() => {
+ store = createStore();
+ store.dispatch('setInitialState', { helpPagePath });
+ wrapper = mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders an help link dependant on the helphPagePath', () => {
+ expect(findHelpLink().attributes('href')).toBe(helpPagePath);
+ });
+});
diff --git a/spec/frontend/registry/settings/stores/actions_spec.js b/spec/frontend/registry/settings/stores/actions_spec.js
new file mode 100644
index 00000000000..205c3a6ee21
--- /dev/null
+++ b/spec/frontend/registry/settings/stores/actions_spec.js
@@ -0,0 +1,20 @@
+import * as actions from '~/registry/settings/stores/actions';
+import * as types from '~/registry/settings/stores/mutation_types';
+import testAction from 'helpers/vuex_action_helper';
+
+jest.mock('~/flash.js');
+
+describe('Actions Registry Store', () => {
+ describe('setInitialState', () => {
+ it('should set the initial state', done => {
+ testAction(
+ actions.setInitialState,
+ 'foo',
+ {},
+ [{ type: types.SET_INITIAL_STATE, payload: 'foo' }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/registry/settings/stores/mutations_spec.js b/spec/frontend/registry/settings/stores/mutations_spec.js
new file mode 100644
index 00000000000..421cd3f13cb
--- /dev/null
+++ b/spec/frontend/registry/settings/stores/mutations_spec.js
@@ -0,0 +1,21 @@
+import mutations from '~/registry/settings/stores/mutations';
+import * as types from '~/registry/settings/stores/mutation_types';
+import createState from '~/registry/settings/stores/state';
+
+describe('Mutations Registry Store', () => {
+ let mockState;
+
+ beforeEach(() => {
+ mockState = createState();
+ });
+
+ describe('SET_INITIAL_STATE', () => {
+ it('should set the initial state', () => {
+ const payload = { helpPagePath: 'foo', registrySettingsEndpoint: 'bar' };
+ const expectedState = { ...mockState, ...payload };
+ mutations[types.SET_INITIAL_STATE](mockState, payload);
+
+ expect(mockState.endpoint).toEqual(expectedState.endpoint);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js b/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js
new file mode 100644
index 00000000000..f8f4cb627dd
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js
@@ -0,0 +1,32 @@
+import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
+
+const deploymentMockData = {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/review-apps/environments/15',
+ stop_url: '/root/review-apps/environments/15/stop',
+ metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
+ external_url: 'http://gitlab.com.',
+ external_url_formatted: 'gitlab',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ details: {},
+ status: SUCCESS,
+ changes: [
+ {
+ path: 'index.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
+ },
+ {
+ path: 'imgs/gallery.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
+ },
+ {
+ path: 'about/',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
+ },
+ ],
+};
+
+export default deploymentMockData;
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
new file mode 100644
index 00000000000..78e086e473d
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
@@ -0,0 +1,194 @@
+import { mount } from '@vue/test-utils';
+import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue';
+import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue';
+import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
+import DeploymentStopButton from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue';
+import {
+ CREATED,
+ RUNNING,
+ SUCCESS,
+ FAILED,
+ CANCELED,
+} from '~/vue_merge_request_widget/components/deployment/constants';
+import deploymentMockData from './deployment_mock_data';
+
+const deployDetail = {
+ playable_build: {
+ retry_path: '/root/test-deployments/-/jobs/1131/retry',
+ play_path: '/root/test-deployments/-/jobs/1131/play',
+ },
+ isManual: true,
+};
+
+describe('Deployment component', () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ // This destroys any wrappers created before a nested call to factory reassigns it
+ if (wrapper && wrapper.destroy) {
+ wrapper.destroy();
+ }
+ wrapper = mount(DeploymentComponent, {
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ factory({
+ propsData: {
+ deployment: deploymentMockData,
+ showMetrics: false,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('always renders DeploymentInfo', () => {
+ expect(wrapper.find(DeploymentInfo).exists()).toBe(true);
+ });
+
+ describe('status message and buttons', () => {
+ const noActions = [];
+ const noDetails = { isManual: false };
+ const deployGroup = [DeploymentViewButton, DeploymentStopButton];
+
+ describe.each`
+ status | previous | deploymentDetails | text | actionButtons
+ ${CREATED} | ${true} | ${deployDetail} | ${'Can deploy manually to'} | ${deployGroup}
+ ${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${deployGroup}
+ ${CREATED} | ${false} | ${deployDetail} | ${'Can deploy manually to'} | ${noActions}
+ ${CREATED} | ${false} | ${noDetails} | ${'Will deploy to'} | ${noActions}
+ ${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${deployGroup}
+ ${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${deployGroup}
+ ${RUNNING} | ${false} | ${deployDetail} | ${'Deploying to'} | ${noActions}
+ ${RUNNING} | ${false} | ${noDetails} | ${'Deploying to'} | ${noActions}
+ ${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${deployGroup}
+ ${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${deployGroup}
+ ${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${deployGroup}
+ ${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${deployGroup}
+ ${FAILED} | ${true} | ${deployDetail} | ${'Failed to deploy to'} | ${deployGroup}
+ ${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${deployGroup}
+ ${FAILED} | ${false} | ${deployDetail} | ${'Failed to deploy to'} | ${noActions}
+ ${FAILED} | ${false} | ${noDetails} | ${'Failed to deploy to'} | ${noActions}
+ ${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deploy to'} | ${deployGroup}
+ ${CANCELED} | ${true} | ${noDetails} | ${'Canceled deploy to'} | ${deployGroup}
+ ${CANCELED} | ${false} | ${deployDetail} | ${'Canceled deploy to'} | ${noActions}
+ ${CANCELED} | ${false} | ${noDetails} | ${'Canceled deploy to'} | ${noActions}
+ `(
+ '$status + previous: $previous + manual: $deploymentDetails.isManual',
+ ({ status, previous, deploymentDetails, text, actionButtons }) => {
+ beforeEach(() => {
+ const previousOrSuccess = Boolean(previous || status === SUCCESS);
+ const updatedDeploymentData = {
+ status,
+ deployed_at: previous ? deploymentMockData.deployed_at : null,
+ deployed_at_formatted: previous ? deploymentMockData.deployed_at_formatted : null,
+ external_url: previousOrSuccess ? deploymentMockData.external_url : null,
+ external_url_formatted: previousOrSuccess
+ ? deploymentMockData.external_url_formatted
+ : null,
+ stop_url: previousOrSuccess ? deploymentMockData.stop_url : null,
+ details: deploymentDetails,
+ };
+
+ factory({
+ propsData: {
+ showMetrics: false,
+ deployment: {
+ ...deploymentMockData,
+ ...updatedDeploymentData,
+ },
+ },
+ });
+ });
+
+ it(`renders the text: ${text}`, () => {
+ expect(wrapper.find(DeploymentInfo).text()).toContain(text);
+ });
+
+ if (actionButtons.length > 0) {
+ describe('renders the expected button group', () => {
+ actionButtons.forEach(button => {
+ it(`renders ${button.name}`, () => {
+ expect(wrapper.find(button).exists()).toBe(true);
+ });
+ });
+ });
+ }
+
+ if (actionButtons.length === 0) {
+ describe('does not render the button group', () => {
+ [DeploymentViewButton, DeploymentStopButton].forEach(button => {
+ it(`does not render ${button.name}`, () => {
+ expect(wrapper.find(button).exists()).toBe(false);
+ });
+ });
+ });
+ }
+
+ if (actionButtons.includes(DeploymentViewButton)) {
+ it('renders the View button with expected text', () => {
+ if (status === SUCCESS) {
+ expect(wrapper.find(DeploymentViewButton).text()).toContain('View app');
+ } else {
+ expect(wrapper.find(DeploymentViewButton).text()).toContain('View previous app');
+ }
+ });
+ }
+ },
+ );
+ });
+
+ describe('hasExternalUrls', () => {
+ describe('when deployment has both external_url_formatted and external_url', () => {
+ it('should return true', () => {
+ expect(wrapper.vm.hasExternalUrls).toEqual(true);
+ });
+
+ it('should render the View Button', () => {
+ expect(wrapper.find(DeploymentViewButton).exists()).toBe(true);
+ });
+ });
+
+ describe('when deployment has no external_url_formatted', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ deployment: { ...deploymentMockData, external_url_formatted: null },
+ showMetrics: false,
+ },
+ });
+ });
+
+ it('should return false', () => {
+ expect(wrapper.vm.hasExternalUrls).toEqual(false);
+ });
+
+ it('should not render the View Button', () => {
+ expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
+ });
+ });
+
+ describe('when deployment has no external_url', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ deployment: { ...deploymentMockData, external_url: null },
+ showMetrics: false,
+ },
+ });
+ });
+
+ it('should return false', () => {
+ expect(wrapper.vm.hasExternalUrls).toEqual(false);
+ });
+
+ it('should not render the View Button', () => {
+ expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
new file mode 100644
index 00000000000..6e3c6f64c68
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
@@ -0,0 +1,118 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
+import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
+import deploymentMockData from './deployment_mock_data';
+
+describe('Deployment View App button', () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = mount(localVue.extend(DeploymentViewButton), {
+ localVue,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ factory({
+ propsData: {
+ deployment: deploymentMockData,
+ isCurrent: true,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('text', () => {
+ describe('when app is current', () => {
+ it('shows View app', () => {
+ expect(wrapper.find(ReviewAppLink).text()).toContain('View app');
+ });
+ });
+
+ describe('when app is not current', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ deployment: deploymentMockData,
+ isCurrent: false,
+ },
+ });
+ });
+
+ it('shows View Previous app', () => {
+ expect(wrapper.find(ReviewAppLink).text()).toContain('View previous app');
+ });
+ });
+ });
+
+ describe('without changes', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ deployment: { ...deploymentMockData, changes: null },
+ isCurrent: false,
+ },
+ });
+ });
+
+ it('renders the link to the review app without dropdown', () => {
+ expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false);
+ });
+ });
+
+ describe('with a single change', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] },
+ isCurrent: false,
+ },
+ });
+ });
+
+ it('renders the link to the review app without dropdown', () => {
+ expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false);
+ });
+
+ it('renders the link to the review app linked to to the first change', () => {
+ const expectedUrl = deploymentMockData.changes[0].external_url;
+ const deployUrl = wrapper.find('.js-deploy-url');
+
+ expect(deployUrl.attributes().href).not.toBeNull();
+ expect(deployUrl.attributes().href).toEqual(expectedUrl);
+ });
+ });
+
+ describe('with multiple changes', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ deployment: deploymentMockData,
+ isCurrent: false,
+ },
+ });
+ });
+
+ it('renders the link to the review app with dropdown', () => {
+ expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(true);
+ });
+
+ it('renders all the links to the review apps', () => {
+ const allUrls = wrapper.findAll('.js-deploy-url-menu-item').wrappers;
+ const expectedUrls = deploymentMockData.changes.map(change => change.external_url);
+
+ expectedUrls.forEach((expectedUrl, idx) => {
+ const deployUrl = allUrls[idx];
+
+ expect(deployUrl.attributes().href).not.toBeNull();
+ expect(deployUrl.attributes().href).toEqual(expectedUrl);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
deleted file mode 100644
index 1949bee1406..00000000000
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ /dev/null
@@ -1,313 +0,0 @@
-import Vue from 'vue';
-import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue';
-import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
-import { getTimeago } from '~/lib/utils/datetime_utility';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
-describe('Deployment component', () => {
- const Component = Vue.extend(deploymentComponent);
- let deploymentMockData;
-
- beforeEach(() => {
- deploymentMockData = {
- id: 15,
- name: 'review/diplo',
- url: '/root/review-apps/environments/15',
- stop_url: '/root/review-apps/environments/15/stop',
- metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
- metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
- external_url: 'http://gitlab.com.',
- external_url_formatted: 'gitlab',
- deployed_at: '2017-03-22T22:44:42.258Z',
- deployed_at_formatted: 'Mar 22, 2017 10:44pm',
- changes: [
- {
- path: 'index.html',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
- },
- {
- path: 'imgs/gallery.html',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
- },
- {
- path: 'about/',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
- },
- ],
- };
- });
-
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('', () => {
- beforeEach(() => {
- vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true });
- });
-
- describe('deployTimeago', () => {
- it('return formatted date', () => {
- const readable = getTimeago().format(deploymentMockData.deployed_at);
-
- expect(vm.deployTimeago).toEqual(readable);
- });
- });
-
- describe('hasExternalUrls', () => {
- it('should return true', () => {
- expect(vm.hasExternalUrls).toEqual(true);
- });
-
- it('should return false when deployment has no external_url_formatted', () => {
- vm.deployment.external_url_formatted = null;
-
- expect(vm.hasExternalUrls).toEqual(false);
- });
-
- it('should return false when deployment has no external_url', () => {
- vm.deployment.external_url = null;
-
- expect(vm.hasExternalUrls).toEqual(false);
- });
- });
-
- describe('hasDeploymentTime', () => {
- it('should return true', () => {
- expect(vm.hasDeploymentTime).toEqual(true);
- });
-
- it('should return false when deployment has no deployed_at', () => {
- vm.deployment.deployed_at = null;
-
- expect(vm.hasDeploymentTime).toEqual(false);
- });
-
- it('should return false when deployment has no deployed_at_formatted', () => {
- vm.deployment.deployed_at_formatted = null;
-
- expect(vm.hasDeploymentTime).toEqual(false);
- });
- });
-
- describe('hasDeploymentMeta', () => {
- it('should return true', () => {
- expect(vm.hasDeploymentMeta).toEqual(true);
- });
-
- it('should return false when deployment has no url', () => {
- vm.deployment.url = null;
-
- expect(vm.hasDeploymentMeta).toEqual(false);
- });
-
- it('should return false when deployment has no name', () => {
- vm.deployment.name = null;
-
- expect(vm.hasDeploymentMeta).toEqual(false);
- });
- });
-
- describe('stopEnvironment', () => {
- const url = '/foo/bar';
- const returnPromise = () =>
- new Promise(resolve => {
- resolve({
- data: {
- redirect_url: url,
- },
- });
- });
- const mockStopEnvironment = () => {
- vm.stopEnvironment(deploymentMockData);
- return vm;
- };
-
- it('should show a confirm dialog and call service.stopEnvironment when confirmed', done => {
- spyOn(window, 'confirm').and.returnValue(true);
- spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
- const visitUrl = spyOnDependency(deploymentComponent, 'visitUrl').and.returnValue(true);
- vm = mockStopEnvironment();
-
- expect(window.confirm).toHaveBeenCalled();
- expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
- setTimeout(() => {
- expect(visitUrl).toHaveBeenCalledWith(url);
- done();
- }, 333);
- });
-
- it('should show a confirm dialog but should not work if the dialog is rejected', () => {
- spyOn(window, 'confirm').and.returnValue(false);
- spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false));
- vm = mockStopEnvironment();
-
- expect(window.confirm).toHaveBeenCalled();
- expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
- });
- });
-
- it('renders deployment name', () => {
- expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(
- deploymentMockData.url,
- );
-
- expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name);
- });
-
- it('renders external URL', () => {
- expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(
- deploymentMockData.external_url,
- );
-
- expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app');
- });
-
- it('renders stop button', () => {
- expect(vm.$el.querySelector('.btn')).not.toBeNull();
- });
-
- it('renders deployment time', () => {
- expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago);
- });
-
- it('renders metrics component', () => {
- expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull();
- });
- });
-
- describe('with showMetrics enabled', () => {
- beforeEach(() => {
- vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true });
- });
-
- it('shows metrics', () => {
- expect(vm.$el).toContainElement('.js-mr-memory-usage');
- });
- });
-
- describe('with showMetrics disabled', () => {
- beforeEach(() => {
- vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: false });
- });
-
- it('hides metrics', () => {
- expect(vm.$el).not.toContainElement('.js-mr-memory-usage');
- });
- });
-
- describe('without changes', () => {
- beforeEach(() => {
- delete deploymentMockData.changes;
-
- vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true });
- });
-
- it('renders the link to the review app without dropdown', () => {
- expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
- expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull();
- });
- });
-
- describe('with a single change', () => {
- beforeEach(() => {
- deploymentMockData.changes = deploymentMockData.changes.slice(0, 1);
-
- vm = mountComponent(Component, {
- deployment: { ...deploymentMockData },
- showMetrics: true,
- });
- });
-
- it('renders the link to the review app without dropdown', () => {
- expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
- expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull();
- });
-
- it('renders the link to the review app linked to to the first change', () => {
- const expectedUrl = deploymentMockData.changes[0].external_url;
- const deployUrl = vm.$el.querySelector('.js-deploy-url');
-
- expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
- expect(deployUrl).not.toBeNull();
- expect(deployUrl.href).toEqual(expectedUrl);
- });
- });
-
- describe('deployment status', () => {
- describe('running', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- deployment: Object.assign({}, deploymentMockData, { status: 'running' }),
- showMetrics: true,
- });
- });
-
- it('renders information about running deployment', () => {
- expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to');
- });
-
- it('renders disabled stop button', () => {
- expect(vm.$el.querySelector('.js-stop-env').getAttribute('disabled')).toBe('disabled');
- });
- });
-
- describe('success', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- deployment: Object.assign({}, deploymentMockData, { status: 'success' }),
- showMetrics: true,
- });
- });
-
- it('renders information about finished deployment', () => {
- expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to');
- });
- });
-
- describe('failed', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- deployment: Object.assign({}, deploymentMockData, { status: 'failed' }),
- showMetrics: true,
- });
- });
-
- it('renders information about finished deployment', () => {
- expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain(
- 'Failed to deploy to',
- );
- });
- });
-
- describe('created', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- deployment: Object.assign({}, deploymentMockData, { status: 'created' }),
- showMetrics: true,
- });
- });
-
- it('renders information about created deployment', () => {
- expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Will deploy to');
- });
- });
-
- describe('canceled', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- deployment: Object.assign({}, deploymentMockData, { status: 'canceled' }),
- showMetrics: true,
- });
- });
-
- it('renders information about canceled deployment', () => {
- expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain(
- 'Failed to deploy to',
- );
- });
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_stop_button_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_stop_button_spec.js
new file mode 100644
index 00000000000..6a6d8279c2c
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/deployment_stop_button_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import deploymentStopComponent from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue';
+import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
+import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Deployment component', () => {
+ const Component = Vue.extend(deploymentStopComponent);
+ let deploymentMockData;
+
+ beforeEach(() => {
+ deploymentMockData = {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/review-apps/environments/15',
+ stop_url: '/root/review-apps/environments/15/stop',
+ metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
+ external_url: 'http://gitlab.com.',
+ external_url_formatted: 'gitlab',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ deployment_manual_actions: [],
+ status: SUCCESS,
+ changes: [
+ {
+ path: 'index.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
+ },
+ {
+ path: 'imgs/gallery.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
+ },
+ {
+ path: 'about/',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
+ },
+ ],
+ };
+ });
+
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ stopUrl: deploymentMockData.stop_url,
+ isDeployInProgress: false,
+ });
+ });
+
+ describe('stopEnvironment', () => {
+ const url = '/foo/bar';
+ const returnPromise = () =>
+ new Promise(resolve => {
+ resolve({
+ data: {
+ redirect_url: url,
+ },
+ });
+ });
+ const mockStopEnvironment = () => {
+ vm.stopEnvironment(deploymentMockData);
+ return vm;
+ };
+
+ it('should show a confirm dialog and call service.stopEnvironment when confirmed', done => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
+ const visitUrl = spyOnDependency(deploymentStopComponent, 'visitUrl').and.returnValue(true);
+ vm = mockStopEnvironment();
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
+ setTimeout(() => {
+ expect(visitUrl).toHaveBeenCalledWith(url);
+ done();
+ }, 333);
+ });
+
+ it('should show a confirm dialog but should not work if the dialog is rejected', () => {
+ spyOn(window, 'confirm').and.returnValue(false);
+ spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false));
+ vm = mockStopEnvironment();
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index 99de320ca2b..d15c3552b4a 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import MemoryUsage from '~/vue_merge_request_widget/components/memory_usage.vue';
+import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
diff --git a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
index 069bc14e01e..d8d84d087bc 100644
--- a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
@@ -8,6 +8,7 @@ describe('review app link', () => {
const props = {
link: '/review',
cssClass: 'js-link',
+ isCurrent: true,
};
let vm;
let el;
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 40bec001ffe..ff34dafa660 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -1,3 +1,5 @@
+import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
+
export default {
id: 132,
iid: 22,
@@ -290,15 +292,20 @@ export const mockStore = {
name: 'bogus',
external_url: 'https://fake.com',
external_url_formatted: 'https://fake.com',
+ status: SUCCESS,
},
{
id: 1,
name: 'bogus-docs',
external_url: 'https://fake.com',
external_url_formatted: 'https://fake.com',
+ status: SUCCESS,
},
],
- postMergeDeployments: [{ id: 0, name: 'prod' }, { id: 1, name: 'prod-docs' }],
+ postMergeDeployments: [
+ { id: 0, name: 'prod', status: SUCCESS },
+ { id: 1, name: 'prod-docs', status: SUCCESS },
+ ],
troubleshootingDocsPath: 'troubleshooting-docs-path',
ciStatus: 'ci-status',
hasCI: true,
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 30e0504e4e1..604b21e77fe 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -6,6 +6,7 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './mock_data';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
+import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
const returnPromise = data =>
new Promise(resolve => {
@@ -277,7 +278,9 @@ describe('mrWidgetOptions', () => {
describe('fetchDeployments', () => {
it('should fetch deployments', done => {
- spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }]));
+ spyOn(vm.service, 'fetchDeployments').and.returnValue(
+ returnPromise([{ id: 1, status: SUCCESS }]),
+ );
vm.fetchPreMergeDeployments();
@@ -554,7 +557,7 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes,
- status: 'success',
+ status: SUCCESS,
};
beforeEach(done => {
diff --git a/spec/lib/api/entities/release_spec.rb b/spec/lib/api/entities/release_spec.rb
new file mode 100644
index 00000000000..729a69347cb
--- /dev/null
+++ b/spec/lib/api/entities/release_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Entities::Release do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:release) { create(:release, :with_evidence, project: project) }
+ let(:user) { create(:user) }
+ let(:entity) { described_class.new(release, current_user: user) }
+
+ subject { entity.as_json }
+
+ describe 'evidence' do
+ context 'when the current user can download code' do
+ it 'exposes the evidence sha and the json path' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :download_code, project).and_return(true)
+
+ expect(subject[:evidence_sha]).to eq(release.evidence_sha)
+ expect(subject[:assets][:evidence_file_path]).to eq(
+ Gitlab::Routing.url_helpers.evidence_project_release_url(project,
+ release.tag,
+ format: :json)
+ )
+ end
+ end
+
+ context 'when the current user cannot download code' do
+ it 'does not expose any evidence data' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :download_code, project).and_return(false)
+
+ expect(subject.keys).not_to include(:evidence_sha)
+ expect(subject[:assets].keys).not_to include(:evidence_file_path)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 95435c6dabd..03340f34305 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -4063,4 +4063,54 @@ describe Ci::Build do
expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
end
end
+
+ describe '#execute_hooks' do
+ context 'with project hooks' do
+ before do
+ create(:project_hook, project: project, job_events: true)
+ end
+
+ it 'execute hooks' do
+ expect_any_instance_of(ProjectHook).to receive(:async_execute)
+
+ build.execute_hooks
+ end
+ end
+
+ context 'without relevant project hooks' do
+ before do
+ create(:project_hook, project: project, job_events: false)
+ end
+
+ it 'does not execute a hook' do
+ expect_any_instance_of(ProjectHook).not_to receive(:async_execute)
+
+ build.execute_hooks
+ end
+ end
+
+ context 'with project services' do
+ before do
+ create(:service, active: true, job_events: true, project: project)
+ end
+
+ it 'execute services' do
+ expect_any_instance_of(Service).to receive(:async_execute)
+
+ build.execute_hooks
+ end
+ end
+
+ context 'without relevant project services' do
+ before do
+ create(:service, active: true, job_events: false, project: project)
+ end
+
+ it 'execute services' do
+ expect_any_instance_of(Service).not_to receive(:async_execute)
+
+ build.execute_hooks
+ end
+ end
+ end
end
diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb
index 2c012f080dd..38f441fbc4d 100644
--- a/spec/services/cohorts_service_spec.rb
+++ b/spec/services/cohorts_service_spec.rb
@@ -22,73 +22,73 @@ describe CohortsService do
expected_cohorts = [
{
registration_month: month_start(11),
- activity_months: Array.new(12) { { total: 0, percentage: 0 } },
+ activity_months: Array.new(11) { { total: 0, percentage: 0 } },
total: 0,
inactive: 0
},
{
registration_month: month_start(10),
- activity_months: [{ total: 2, percentage: 100 }] + Array.new(10) { { total: 1, percentage: 50 } },
+ activity_months: Array.new(10) { { total: 1, percentage: 50 } },
total: 2,
inactive: 0
},
{
registration_month: month_start(9),
- activity_months: Array.new(10) { { total: 0, percentage: 0 } },
+ activity_months: Array.new(9) { { total: 0, percentage: 0 } },
total: 0,
inactive: 0
},
{
registration_month: month_start(8),
- activity_months: [{ total: 2, percentage: 100 }] + Array.new(8) { { total: 1, percentage: 50 } },
+ activity_months: Array.new(8) { { total: 1, percentage: 50 } },
total: 2,
inactive: 0
},
{
registration_month: month_start(7),
- activity_months: Array.new(8) { { total: 0, percentage: 0 } },
+ activity_months: Array.new(7) { { total: 0, percentage: 0 } },
total: 0,
inactive: 0
},
{
registration_month: month_start(6),
- activity_months: [{ total: 2, percentage: 100 }] + Array.new(6) { { total: 1, percentage: 50 } },
+ activity_months: Array.new(6) { { total: 1, percentage: 50 } },
total: 2,
inactive: 0
},
{
registration_month: month_start(5),
- activity_months: Array.new(6) { { total: 0, percentage: 0 } },
+ activity_months: Array.new(5) { { total: 0, percentage: 0 } },
total: 0,
inactive: 0
},
{
registration_month: month_start(4),
- activity_months: [{ total: 2, percentage: 100 }] + Array.new(4) { { total: 1, percentage: 50 } },
+ activity_months: Array.new(4) { { total: 1, percentage: 50 } },
total: 2,
inactive: 0
},
{
registration_month: month_start(3),
- activity_months: Array.new(4) { { total: 0, percentage: 0 } },
+ activity_months: Array.new(3) { { total: 0, percentage: 0 } },
total: 0,
inactive: 0
},
{
registration_month: month_start(2),
- activity_months: [{ total: 2, percentage: 100 }] + Array.new(2) { { total: 1, percentage: 50 } },
+ activity_months: Array.new(2) { { total: 1, percentage: 50 } },
total: 2,
inactive: 0
},
{
registration_month: month_start(1),
- activity_months: Array.new(2) { { total: 0, percentage: 0 } },
+ activity_months: Array.new(1) { { total: 0, percentage: 0 } },
total: 0,
inactive: 0
},
{
registration_month: month_start(0),
- activity_months: [{ total: 2, percentage: 100 }],
+ activity_months: [],
total: 2,
inactive: 1
}