Commit 87231973 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 8f764d21
......@@ -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
......
......@@ -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)
......
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();
});
<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>
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', {});
},
});
};
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 () => {};
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();
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
// to avoid eslint error until more actions are added to the store
export default () => {};
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, initialState) {
state.helpPagePath = initialState.helpPagePath;
state.registrySettingsEndpoint = initialState.registrySettingsEndpoint;
},
};
export default () => ({
/*
* Help page path to generate the link
*/
helpPagePath: '',
/*
* Settings endpoint to call to fetch and update the settings
*/
registrySettingsEndpoint: '',
});
<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>
// 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';
<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>
<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>
<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>
<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>
<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',
......
<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';
......
<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>
......@@ -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';
......
......@@ -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|
......
......@@ -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
......
......@@ -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
}
......
- 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'
......
#js-registry-settings{ data: { registry_settings_endpoint: '',
help_page_path: help_page_path('user/project/operations/linking_to_an_external_dashboard') } }
......@@ -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'
---
title: Clean up the cohorts table
merge_request: 20779
author:
type: changed
---
title: Update information and button text for deployment footer
merge_request: 18918
author:
type: changed
---
title: Evidence - Added restriction for guest on Release page
merge_request: 21102
author:
type: changed
---
title: Reduce Gitaly calls in BuildHooksWorker
merge_request: 20365
author:
type: performance
......@@ -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
......@@ -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)
......
# 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
......@@ -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
......
......@@ -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
```
......@@ -75,6 +75,7 @@ cannot be used as job names**:
- `after_script`
- `variables`
- `cache`
- `include`
### Using reserved keywords
......
......@@ -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
......
......@@ -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.
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
# 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
......@@ -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 ""
......
......@@ -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)
......
......@@ -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
......
......@@ -184,6 +184,7 @@ describe Projects::ReleasesController do
sign_in(user)
end
context 'when the user is a developer' do
it 'returns the correct evidence summary as a json' do
subject
......@@ -191,9 +192,11 @@ describe Projects::ReleasesController do
end
context 'when the release was created before evidence existed' do
it 'returns an empty json' do
before do
release.evidence.destroy
end
it 'returns an empty json' do
subject
expect(json_response).to eq({})
......@@ -201,6 +204,23 @@ describe Projects::ReleasesController do
end
end
context 'when the user is a guest for the project' do
before do
project.add_guest(user)
end
context 'when the project is private' do
let(:project) { private_project }
it_behaves_like 'not found'
end
context 'when the project is public' do
it_behaves_like 'successful request'
end
end
end
private
def get_index
......
......@@ -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],
......
......@@ -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
......
......@@ -45,6 +45,7 @@ describe('Issuable component', () => {
...props,
},
sync: false,
attachToDocument: true,
});
};
......
......@@ -49,6 +49,7 @@ describe('Issuables list component', () => {
},
localVue,
sync: false,
attachToDocument: true,
});
};
......
// 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>
`;
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);
});
});
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,
);
});
});
});
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);
});
});
});
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;
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);
});
});
});
});
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);
});
});
});
});
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();
});
});
});
});
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';
......
......@@ -8,6 +8,7 @@ describe('review app link', () => {
const props = {
link: '/review',
cssClass: 'js-link',
isCurrent: true,
};
let vm;
let el;
......
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,
......
......@@ -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 => {
......
# 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
......@@ -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
......@@ -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
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment