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: ...@@ -30,6 +30,11 @@ rules:
no-else-return: no-else-return:
- error - error
- allowElseIf: true - allowElseIf: true
import/no-unresolved:
- error
- ignore:
# https://gitlab.com/gitlab-org/gitlab/issues/38226
- '^ee_component/'
import/no-useless-path-segments: off import/no-useless-path-segments: off
import/order: off import/order: off
lines-between-class-members: off lines-between-class-members: off
......
...@@ -748,7 +748,7 @@ GEM ...@@ -748,7 +748,7 @@ GEM
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.1.1) public_suffix (3.1.1)
puma (4.3.0) puma (4.3.1)
nio4r (~> 2.0) nio4r (~> 2.0)
puma_worker_killer (0.1.1) puma_worker_killer (0.1.1)
get_process_mem (~> 0.2) get_process_mem (~> 0.2)
......
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values'; import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels // Initialize expandable settings panels
...@@ -32,4 +33,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -32,4 +33,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none'; if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked); 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> <script>
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import statusCodes from '../../lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '../../lib/utils/number_utils'; import { bytesToMiB } from '~/lib/utils/number_utils';
import { backOff } from '../../lib/utils/common_utils'; import { backOff } from '~/lib/utils/common_utils';
import MemoryGraph from '../../vue_shared/components/memory_graph.vue'; import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
import MRWidgetService from '../services/mr_widget_service'; import MRWidgetService from '../../services/mr_widget_service';
export default { export default {
name: 'MemoryUsage', name: 'MemoryUsage',
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import ArtifactsApp from './artifacts_list_app.vue'; 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 MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue'; import MrWidgetPipeline from './mr_widget_pipeline.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
......
<script> <script>
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
...@@ -6,15 +7,24 @@ export default { ...@@ -6,15 +7,24 @@ export default {
Icon, Icon,
}, },
props: { props: {
link: { cssClass: {
type: String, type: String,
required: true, required: true,
}, },
cssClass: { isCurrent: {
type: Boolean,
required: true,
},
link: {
type: String, type: String,
required: true, required: true,
}, },
}, },
computed: {
linkText() {
return this.isCurrent ? __('View app') : __('View previous app');
},
},
}; };
</script> </script>
<template> <template>
...@@ -26,6 +36,6 @@ export default { ...@@ -26,6 +36,6 @@ export default {
data-track-event="open_review_app" data-track-event="open_review_app"
data-track-label="review_app" data-track-label="review_app"
> >
{{ __('View app') }} <icon class="fgray" name="external-link" /> {{ linkText }} <icon class="fgray" name="external-link" />
</a> </a>
</template> </template>
...@@ -10,7 +10,7 @@ import createFlash from '../flash'; ...@@ -10,7 +10,7 @@ import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue'; import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.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 WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue'; import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import MergedState from './components/states/mr_widget_merged.vue'; import MergedState from './components/states/mr_widget_merged.vue';
......
...@@ -10,6 +10,7 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -10,6 +10,7 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_evidence_collection, project) push_frontend_feature_flag(:release_evidence_collection, project)
end end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_download_code!, only: [:evidence]
def index def index
respond_to do |format| respond_to do |format|
......
...@@ -662,9 +662,8 @@ module Ci ...@@ -662,9 +662,8 @@ module Ci
def execute_hooks def execute_hooks
return unless project return unless project
build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :job_hooks) if project.has_active_hooks?(:job_hooks)
project.execute_hooks(build_data.dup, :job_hooks) project.execute_services(build_data.dup, :job_hooks) if project.has_active_services?(:job_hooks)
project.execute_services(build_data.dup, :job_hooks)
end end
def browsable_artifacts? def browsable_artifacts?
...@@ -873,6 +872,10 @@ module Ci ...@@ -873,6 +872,10 @@ module Ci
private private
def build_data
@build_data ||= Gitlab::DataBuilder::Build.build(self)
end
def successful_deployment_status def successful_deployment_status
if deployment&.last? if deployment&.last?
:last :last
......
...@@ -38,7 +38,7 @@ class CohortsService ...@@ -38,7 +38,7 @@ class CohortsService
{ {
registration_month: registration_month, registration_month: registration_month,
activity_months: activity_months, activity_months: activity_months[1..-1],
total: activity_months.first[:total], total: activity_months.first[:total],
inactive: inactive inactive: inactive
} }
......
- number_of_data_columns = @cohorts[:months_included] - 1
.bs-callout.clearfix .bs-callout.clearfix
%p %p
User cohorts are shown for the last #{@cohorts[:months_included]} = 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] }
months. Only users with activity are counted in the cohort total; inactive
users are counted separately.
= link_to icon('question-circle'), help_page_path('user/instance_statistics/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank' = 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 %table.table
%thead %thead
%tr %tr
%th Registration month %th.border-right.pt-4{ colspan: 3 }
%th Inactive users %th.font-weight-bold.pt-4{ colspan: number_of_data_columns }
%th Cohort total = s_("Cohorts|Returning users")
- @cohorts[:months_included].times do |i| %tr
%th Month #{i} %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 %tbody
- @cohorts[:cohorts].each do |cohort| - @cohorts[:cohorts].each do |cohort|
%tr %tr
%td= cohort[:registration_month] %td= cohort[:registration_month]
%td= cohort[:inactive] %td= cohort[:inactive]
%td= cohort[:total] %td.border-right= cohort[:total]
- cohort[:activity_months].each do |activity_month| - cohort[:activity_months].each do |activity_month|
%td %td
- next if cohort[:total] == '0' - 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 @@ ...@@ -59,3 +59,14 @@
.settings-content .settings-content
= render 'projects/triggers/index' = 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' ...@@ -7,6 +7,17 @@ require 'gitlab/current_settings'
Gitlab.ee do Gitlab.ee do
require 'elasticsearch/model' 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 ### Modified from elasticsearch-model/lib/elasticsearch/model.rb
[ [
...@@ -32,15 +43,4 @@ Gitlab.ee do ...@@ -32,15 +43,4 @@ Gitlab.ee do
target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super
end end
CODE 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 end
...@@ -88,8 +88,8 @@ def instrument_classes(instrumentation) ...@@ -88,8 +88,8 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Gitlab::Highlight) instrumentation.instrument_instance_methods(Gitlab::Highlight)
Gitlab.ee do Gitlab.ee do
instrumentation.instrument_methods(Elasticsearch::Git::Repository) instrumentation.instrument_instance_methods(Elastic::Latest::GitInstanceProxy)
instrumentation.instrument_instance_methods(Elasticsearch::Git::Repository) instrumentation.instrument_instance_methods(Elastic::Latest::GitClassProxy)
instrumentation.instrument_instance_methods(Search::GlobalService) instrumentation.instrument_instance_methods(Search::GlobalService)
instrumentation.instrument_instance_methods(Search::ProjectService) 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 ...@@ -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" t.index ["upload_id"], name: "index_geo_upload_deleted_events_on_upload_id"
end 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| create_table "gitlab_subscriptions", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
......
...@@ -270,3 +270,15 @@ database: gitlabhq_production ...@@ -270,3 +270,15 @@ database: gitlabhq_production
-------------------------------------------------- --------------------------------------------------
up migration_id migration_name 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**: ...@@ -75,6 +75,7 @@ cannot be used as job names**:
- `after_script` - `after_script`
- `variables` - `variables`
- `cache` - `cache`
- `include`
### Using reserved keywords ### Using reserved keywords
......
...@@ -249,7 +249,7 @@ scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to ...@@ -249,7 +249,7 @@ scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to
catch any warnings. catch any warnings.
If the Rake task is throwing warnings you don't understand, SCSS Lint's 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 ### Fixing issues
......
...@@ -22,7 +22,9 @@ The requirement for adding a new metric is to make each query to have an unique ...@@ -22,7 +22,9 @@ The requirement for adding a new metric is to make each query to have an unique
### Update existing metrics ### 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:** NOTE: **Note:**
If a query metric (which is identified by `id:`) is removed it will not be removed from database by default. 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): ...@@ -348,7 +348,7 @@ project):
echo-js: echo-js:
handler: echo-js handler: echo-js
source: ./echo-js source: ./echo-js
runtime: https://gitlab.com/gitlab-org/serverless/runtimes/nodejs runtime: gitlab/runtimes/nodejs
description: "node.js runtime function" description: "node.js runtime function"
environment: environment:
MY_FUNCTION: echo-js MY_FUNCTION: echo-js
...@@ -379,10 +379,27 @@ subsequent lines contain the function attributes. ...@@ -379,10 +379,27 @@ subsequent lines contain the function attributes.
|-----------|-------------| |-----------|-------------|
| `handler` | The function's name. | | `handler` | The function's name. |
| `source` | Directory with sources of a functions. | | `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. | | `description` | A short description of the function. |
| `environment` | Sets an environment variable for the specific function only. | | `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 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 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 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: ...@@ -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 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` [run a query](prometheus_library/kubernetes.html#metrics-supported), replacing `$CI_ENVIRONMENT_SLUG`
with the name of your environment. 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 [autodeploy]: ../../../topics/autodevops/index.md#auto-deploy
[kubernetes]: https://kubernetes.io [kubernetes]: https://kubernetes.io
......
...@@ -1319,7 +1319,7 @@ module API ...@@ -1319,7 +1319,7 @@ module API
expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? } expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? }
expose :commit_path, expose_nil: false expose :commit_path, expose_nil: false
expose :tag_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 do
expose :assets_count, as: :count do |release, _| expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources] assets_to_exclude = can_download_code? ? [] : [:sources]
...@@ -1329,7 +1329,7 @@ module API ...@@ -1329,7 +1329,7 @@ module API
expose :links, using: Entities::Releases::Link do |release, options| expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted release.links.sorted
end end
expose :evidence_file_path, expose_nil: false expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? }
end end
expose :_links do expose :_links do
expose :merge_requests_url, expose_nil: false 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 "" ...@@ -2942,6 +2942,9 @@ msgstr ""
msgid "Callback URL" msgid "Callback URL"
msgstr "" msgstr ""
msgid "Can deploy manually to"
msgstr ""
msgid "Can override approvers and approvals required per merge request" msgid "Can override approvers and approvals required per merge request"
msgstr "" msgstr ""
...@@ -2969,6 +2972,9 @@ msgstr "" ...@@ -2969,6 +2972,9 @@ msgstr ""
msgid "Cancel this job" msgid "Cancel this job"
msgstr "" msgstr ""
msgid "Canceled deploy to"
msgstr ""
msgid "Cancelling Preview" msgid "Cancelling Preview"
msgstr "" msgstr ""
...@@ -4334,6 +4340,24 @@ msgstr "" ...@@ -4334,6 +4340,24 @@ msgstr ""
msgid "Cohorts" msgid "Cohorts"
msgstr "" 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" msgid "Collapse"
msgstr "" msgstr ""
...@@ -4615,6 +4639,9 @@ msgstr "" ...@@ -4615,6 +4639,9 @@ msgstr ""
msgid "Container Registry" msgid "Container Registry"
msgstr "" msgstr ""
msgid "Container Registry tag expiration policies"
msgstr ""
msgid "Container Scanning" msgid "Container Scanning"
msgstr "" msgstr ""
...@@ -7092,6 +7119,9 @@ msgstr "" ...@@ -7092,6 +7119,9 @@ msgstr ""
msgid "Expiration date" msgid "Expiration date"
msgstr "" 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" msgid "Expired"
msgstr "" msgstr ""
...@@ -12141,6 +12171,9 @@ msgstr "" ...@@ -12141,6 +12171,9 @@ msgstr ""
msgid "Package was removed" msgid "Package was removed"
msgstr "" msgstr ""
msgid "PackageRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
msgid "PackageRegistry|Copy Maven XML" msgid "PackageRegistry|Copy Maven XML"
msgstr "" msgstr ""
...@@ -12180,6 +12213,9 @@ msgstr "" ...@@ -12180,6 +12213,9 @@ msgstr ""
msgid "PackageRegistry|Installation" msgid "PackageRegistry|Installation"
msgstr "" 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." msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr "" msgstr ""
...@@ -12192,12 +12228,18 @@ msgstr "" ...@@ -12192,12 +12228,18 @@ msgstr ""
msgid "PackageRegistry|Package installation" msgid "PackageRegistry|Package installation"
msgstr "" msgstr ""
msgid "PackageRegistry|Read more about the %{helpLinkStart}Container Registry tag retention policies%{helpLinkEnd}"
msgstr ""
msgid "PackageRegistry|Registry Setup" msgid "PackageRegistry|Registry Setup"
msgstr "" msgstr ""
msgid "PackageRegistry|Remove package" msgid "PackageRegistry|Remove package"
msgstr "" msgstr ""
msgid "PackageRegistry|Tag retention policies are designed to:"
msgstr ""
msgid "PackageRegistry|There are no packages yet" msgid "PackageRegistry|There are no packages yet"
msgstr "" msgstr ""
...@@ -19533,6 +19575,9 @@ msgstr "" ...@@ -19533,6 +19575,9 @@ msgstr ""
msgid "View open merge request" msgid "View open merge request"
msgstr "" msgstr ""
msgid "View previous app"
msgstr ""
msgid "View project labels" msgid "View project labels"
msgstr "" msgstr ""
......
...@@ -154,7 +154,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -154,7 +154,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
.and_return(merge_request) .and_return(merge_request)
end 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 get_show_json
json_response.dig('pipeline', 'details', 'stages').tap do |stages| json_response.dig('pipeline', 'details', 'stages').tap do |stages|
...@@ -183,7 +183,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -183,7 +183,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'job is cancelable' do context 'job is cancelable' do
let(:job) { create(:ci_build, :running, pipeline: pipeline) } 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 have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details') expect(response).to match_response_schema('job/job_details')
expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path'])) 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 ...@@ -193,7 +193,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'with web terminal' do context 'with web terminal' do
let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } 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 have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details') expect(response).to match_response_schema('job/job_details')
expect(json_response['terminal_path']).to match(%r{/terminal}) expect(json_response['terminal_path']).to match(%r{/terminal})
...@@ -268,7 +268,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -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 project.add_maintainer(user) # Need to be a maintianer to view cluster.path
end end
it 'exposes the deployment information', :sidekiq_might_not_need_inline do it 'exposes the deployment information' do
get_show_json get_show_json
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
...@@ -292,7 +292,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -292,7 +292,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user) sign_in(user)
end end
it 'user can edit runner', :sidekiq_might_not_need_inline do it 'user can edit runner' do
get_show_json get_show_json
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
...@@ -312,7 +312,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -312,7 +312,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user) sign_in(user)
end end
it 'user can not edit runner', :sidekiq_might_not_need_inline do it 'user can not edit runner' do
get_show_json get_show_json
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
...@@ -331,7 +331,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -331,7 +331,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user) sign_in(user)
end end
it 'user can not edit runner', :sidekiq_might_not_need_inline do it 'user can not edit runner' do
get_show_json get_show_json
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
...@@ -412,7 +412,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -412,7 +412,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'when job has trace' do context 'when job has trace' do
let(:job) { create(:ci_build, :running, :trace_live, pipeline: pipeline) } 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 get_show_json
expect(response).to match_response_schema('job/job_details') expect(response).to match_response_schema('job/job_details')
...@@ -458,7 +458,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -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') create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
end end
context 'user is a maintainer', :sidekiq_might_not_need_inline do context 'user is a maintainer' do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -512,7 +512,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -512,7 +512,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
def get_show_json def get_show_json
expect { get_show(id: job.id, format: :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 end
def get_show(**extra_params) def get_show(**extra_params)
......
...@@ -93,7 +93,7 @@ describe Projects::PipelinesController do ...@@ -93,7 +93,7 @@ describe Projects::PipelinesController do
end end
context 'when performing gitaly calls', :request_store do 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) # Isolate from test preparation (Repository#exists? is also cached in RequestStore)
RequestStore.end! RequestStore.end!
RequestStore.clear! RequestStore.clear!
...@@ -101,8 +101,9 @@ describe Projects::PipelinesController do ...@@ -101,8 +101,9 @@ describe Projects::PipelinesController do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
# ListCommitsByOid, RepositoryExists, HasLocalBranches
expect { get_pipelines_index_json } 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
end end
......
...@@ -184,19 +184,39 @@ describe Projects::ReleasesController do ...@@ -184,19 +184,39 @@ describe Projects::ReleasesController do
sign_in(user) sign_in(user)
end end
it 'returns the correct evidence summary as a json' do context 'when the user is a developer' do
subject 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 end
context 'when the release was created before evidence existed' do context 'when the user is a guest for the project' do
it 'returns an empty json' do before do
release.evidence.destroy 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 end
end end
......
...@@ -43,6 +43,7 @@ describe 'Database schema' do ...@@ -43,6 +43,7 @@ describe 'Database schema' do
geo_nodes: %w[oauth_application_id], geo_nodes: %w[oauth_application_id],
geo_repository_deleted_events: %w[project_id], geo_repository_deleted_events: %w[project_id],
geo_upload_deleted_events: %w[upload_id model_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], import_failures: %w[project_id],
identities: %w[user_id], identities: %w[user_id],
issues: %w[last_edited_by_id state_id], issues: %w[last_edited_by_id state_id],
......
...@@ -96,7 +96,7 @@ describe 'Merge request > User sees deployment widget', :js do ...@@ -96,7 +96,7 @@ describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
wait_for_requests 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') expect(page).not_to have_css('.js-deploy-time')
end end
end end
......
...@@ -45,6 +45,7 @@ describe('Issuable component', () => { ...@@ -45,6 +45,7 @@ describe('Issuable component', () => {
...props, ...props,
}, },
sync: false, sync: false,
attachToDocument: true,
}); });
}; };
......
...@@ -49,6 +49,7 @@ describe('Issuables list component', () => { ...@@ -49,6 +49,7 @@ describe('Issuables list component', () => {
}, },
localVue, localVue,
sync: false, 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 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'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
const url = '/root/acets-review-apps/environments/15/deployments/1/metrics'; const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
......
...@@ -8,6 +8,7 @@ describe('review app link', () => { ...@@ -8,6 +8,7 @@ describe('review app link', () => {
const props = { const props = {
link: '/review', link: '/review',
cssClass: 'js-link', cssClass: 'js-link',
isCurrent: true,
}; };
let vm; let vm;
let el; let el;
......
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
export default { export default {
id: 132, id: 132,
iid: 22, iid: 22,
...@@ -290,15 +292,20 @@ export const mockStore = { ...@@ -290,15 +292,20 @@ export const mockStore = {
name: 'bogus', name: 'bogus',
external_url: 'https://fake.com', external_url: 'https://fake.com',
external_url_formatted: 'https://fake.com', external_url_formatted: 'https://fake.com',
status: SUCCESS,
}, },
{ {
id: 1, id: 1,
name: 'bogus-docs', name: 'bogus-docs',
external_url: 'https://fake.com', external_url: 'https://fake.com',
external_url_formatted: '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', troubleshootingDocsPath: 'troubleshooting-docs-path',
ciStatus: 'ci-status', ciStatus: 'ci-status',
hasCI: true, hasCI: true,
......
...@@ -6,6 +6,7 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; ...@@ -6,6 +6,7 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './mock_data'; import mockData from './mock_data';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
const returnPromise = data => const returnPromise = data =>
new Promise(resolve => { new Promise(resolve => {
...@@ -277,7 +278,9 @@ describe('mrWidgetOptions', () => { ...@@ -277,7 +278,9 @@ describe('mrWidgetOptions', () => {
describe('fetchDeployments', () => { describe('fetchDeployments', () => {
it('should fetch deployments', done => { 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(); vm.fetchPreMergeDeployments();
...@@ -554,7 +557,7 @@ describe('mrWidgetOptions', () => { ...@@ -554,7 +557,7 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z', deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm', deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes, changes,
status: 'success', status: SUCCESS,
}; };
beforeEach(done => { 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 ...@@ -4063,4 +4063,54 @@ describe Ci::Build do
expect(job.invalid_dependencies).to eq([pre_stage_job_invalid]) expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
end end
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 end
...@@ -22,73 +22,73 @@ describe CohortsService do ...@@ -22,73 +22,73 @@ describe CohortsService do
expected_cohorts = [ expected_cohorts = [
{ {
registration_month: month_start(11), 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, total: 0,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(10), 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, total: 2,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(9), 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, total: 0,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(8), 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, total: 2,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(7), 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, total: 0,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(6), 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, total: 2,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(5), 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, total: 0,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(4), 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, total: 2,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(3), 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, total: 0,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(2), 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, total: 2,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(1), 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, total: 0,
inactive: 0 inactive: 0
}, },
{ {
registration_month: month_start(0), registration_month: month_start(0),
activity_months: [{ total: 2, percentage: 100 }], activity_months: [],
total: 2, total: 2,
inactive: 1 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