Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d47f9d2304
commit
aa0f0e9921
125 changed files with 3429 additions and 736 deletions
|
@ -17,10 +17,13 @@ import createFlash from '~/flash';
|
|||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
|
||||
import invalidUrl from '~/lib/utils/invalid_url';
|
||||
|
||||
import DateTimePicker from './date_time_picker/date_time_picker.vue';
|
||||
import GraphGroup from './graph_group.vue';
|
||||
import EmptyState from './empty_state.vue';
|
||||
import GroupEmptyState from './group_empty_state.vue';
|
||||
import DashboardsDropdown from './dashboards_dropdown.vue';
|
||||
|
||||
import TrackEventDirective from '~/vue_shared/directives/track_event';
|
||||
import { getTimeDiff, getAddMetricTrackingOptions } from '../utils';
|
||||
import { metricStates } from '../constants';
|
||||
|
@ -31,16 +34,18 @@ export default {
|
|||
components: {
|
||||
VueDraggable,
|
||||
PanelType,
|
||||
GraphGroup,
|
||||
EmptyState,
|
||||
GroupEmptyState,
|
||||
Icon,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlFormGroup,
|
||||
GlModal,
|
||||
|
||||
DateTimePicker,
|
||||
GraphGroup,
|
||||
EmptyState,
|
||||
GroupEmptyState,
|
||||
DashboardsDropdown,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
|
@ -83,6 +88,10 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
metricsEndpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -140,6 +149,11 @@ export default {
|
|||
required: false,
|
||||
default: invalidUrl,
|
||||
},
|
||||
dashboardsEndpoint: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: invalidUrl,
|
||||
},
|
||||
currentDashboard: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -199,9 +213,6 @@ export default {
|
|||
selectedDashboard() {
|
||||
return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
|
||||
},
|
||||
selectedDashboardText() {
|
||||
return this.selectedDashboard.display_name;
|
||||
},
|
||||
showRearrangePanelsBtn() {
|
||||
return !this.showEmptyState && this.rearrangePanelsAvailable;
|
||||
},
|
||||
|
@ -223,6 +234,7 @@ export default {
|
|||
environmentsEndpoint: this.environmentsEndpoint,
|
||||
deploymentsEndpoint: this.deploymentsEndpoint,
|
||||
dashboardEndpoint: this.dashboardEndpoint,
|
||||
dashboardsEndpoint: this.dashboardsEndpoint,
|
||||
currentDashboard: this.currentDashboard,
|
||||
projectPath: this.projectPath,
|
||||
});
|
||||
|
@ -314,6 +326,13 @@ export default {
|
|||
return !this.getMetricStates(groupKey).includes(metricStates.OK);
|
||||
},
|
||||
getAddMetricTrackingOptions,
|
||||
|
||||
selectDashboard(dashboard) {
|
||||
const params = {
|
||||
dashboard: dashboard.path,
|
||||
};
|
||||
redirectTo(mergeUrlParams(params, window.location.href));
|
||||
},
|
||||
},
|
||||
addMetric: {
|
||||
title: s__('Metrics|Add metric'),
|
||||
|
@ -333,21 +352,14 @@ export default {
|
|||
label-for="monitor-dashboards-dropdown"
|
||||
class="col-sm-12 col-md-6 col-lg-2"
|
||||
>
|
||||
<gl-dropdown
|
||||
<dashboards-dropdown
|
||||
id="monitor-dashboards-dropdown"
|
||||
class="mb-0 d-flex js-dashboards-dropdown"
|
||||
class="mb-0 d-flex"
|
||||
toggle-class="dropdown-menu-toggle"
|
||||
:text="selectedDashboardText"
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-for="dashboard in allDashboards"
|
||||
:key="dashboard.path"
|
||||
:active="dashboard.path === currentDashboard"
|
||||
active-class="is-active"
|
||||
:href="`?dashboard=${dashboard.path}`"
|
||||
>{{ dashboard.display_name || dashboard.path }}</gl-dropdown-item
|
||||
>
|
||||
</gl-dropdown>
|
||||
:default-branch="defaultBranch"
|
||||
:selected-dashboard="selectedDashboard"
|
||||
@selectDashboard="selectDashboard($event)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import {
|
||||
GlAlert,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
GlModal,
|
||||
GlLoadingIcon,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
|
||||
|
||||
const events = {
|
||||
selectDashboard: 'selectDashboard',
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
GlModal,
|
||||
GlLoadingIcon,
|
||||
DuplicateDashboardForm,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
selectedDashboard: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
alert: null,
|
||||
loading: false,
|
||||
form: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('monitoringDashboard', ['allDashboards']),
|
||||
isSystemDashboard() {
|
||||
return this.selectedDashboard.system_dashboard;
|
||||
},
|
||||
selectedDashboardText() {
|
||||
return this.selectedDashboard.display_name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
|
||||
selectDashboard(dashboard) {
|
||||
this.$emit(events.selectDashboard, dashboard);
|
||||
},
|
||||
ok(bvModalEvt) {
|
||||
// Prevent modal from hiding in case submit fails
|
||||
bvModalEvt.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
this.alert = null;
|
||||
this.duplicateSystemDashboard(this.form)
|
||||
.then(createdDashboard => {
|
||||
this.loading = false;
|
||||
this.alert = null;
|
||||
|
||||
// Trigger hide modal as submit is successful
|
||||
this.$refs.duplicateDashboardModal.hide();
|
||||
|
||||
// Dashboards in the default branch become available immediately.
|
||||
// Not so in other branches, so we refresh the current dashboard
|
||||
const dashboard =
|
||||
this.form.branch === this.defaultBranch ? createdDashboard : this.selectedDashboard;
|
||||
this.$emit(events.selectDashboard, dashboard);
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false;
|
||||
this.alert = error;
|
||||
});
|
||||
},
|
||||
hide() {
|
||||
this.alert = null;
|
||||
},
|
||||
formChange(form) {
|
||||
this.form = form;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-dropdown toggle-class="dropdown-menu-toggle" :text="selectedDashboardText">
|
||||
<gl-dropdown-item
|
||||
v-for="dashboard in allDashboards"
|
||||
:key="dashboard.path"
|
||||
:active="dashboard.path === selectedDashboard.path"
|
||||
active-class="is-active"
|
||||
@click="selectDashboard(dashboard)"
|
||||
>
|
||||
{{ dashboard.display_name || dashboard.path }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<template v-if="isSystemDashboard">
|
||||
<gl-dropdown-divider />
|
||||
|
||||
<gl-modal
|
||||
ref="duplicateDashboardModal"
|
||||
modal-id="duplicateDashboardModal"
|
||||
:title="s__('Metrics|Duplicate dashboard')"
|
||||
ok-variant="success"
|
||||
@ok="ok"
|
||||
@hide="hide"
|
||||
>
|
||||
<gl-alert v-if="alert" class="mb-3" variant="danger" @dismiss="alert = null">
|
||||
{{ alert }}
|
||||
</gl-alert>
|
||||
<duplicate-dashboard-form
|
||||
:dashboard="selectedDashboard"
|
||||
:default-branch="defaultBranch"
|
||||
@change="formChange"
|
||||
/>
|
||||
<template #modal-ok>
|
||||
<gl-loading-icon v-if="loading" inline color="light" />
|
||||
{{ loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate') }}
|
||||
</template>
|
||||
</gl-modal>
|
||||
|
||||
<gl-dropdown-item ref="duplicateDashboardItem" v-gl-modal="'duplicateDashboardModal'">
|
||||
{{ s__('Metrics|Duplicate dashboard') }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -0,0 +1,138 @@
|
|||
<script>
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { GlFormGroup, GlFormInput, GlFormRadioGroup, GlFormTextarea } from '@gitlab/ui';
|
||||
|
||||
const defaultFileName = dashboard => dashboard.path.split('/').reverse()[0];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormRadioGroup,
|
||||
GlFormTextarea,
|
||||
},
|
||||
props: {
|
||||
dashboard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
radioVals: {
|
||||
/* Use the default branch (e.g. master) */
|
||||
DEFAULT: 'DEFAULT',
|
||||
/* Create a new branch */
|
||||
NEW: 'NEW',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
dashboard: this.dashboard.path,
|
||||
fileName: defaultFileName(this.dashboard),
|
||||
commitMessage: '',
|
||||
},
|
||||
branchName: '',
|
||||
branchOption: this.$options.radioVals.NEW,
|
||||
branchOptions: [
|
||||
{
|
||||
value: this.$options.radioVals.DEFAULT,
|
||||
html: sprintf(
|
||||
__('Commit to %{branchName} branch'),
|
||||
{
|
||||
branchName: `<strong>${this.defaultBranch}</strong>`,
|
||||
},
|
||||
false,
|
||||
),
|
||||
},
|
||||
{ value: this.$options.radioVals.NEW, text: __('Create new branch') },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
defaultCommitMsg() {
|
||||
return sprintf(s__('Metrics|Create custom dashboard %{fileName}'), {
|
||||
fileName: this.form.fileName,
|
||||
});
|
||||
},
|
||||
fileNameState() {
|
||||
// valid if empty or *.yml
|
||||
return !(this.form.fileName && !this.form.fileName.endsWith('.yml'));
|
||||
},
|
||||
fileNameFeedback() {
|
||||
return !this.fileNameState ? s__('The file name should have a .yml extension') : '';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.change();
|
||||
},
|
||||
methods: {
|
||||
change() {
|
||||
this.$emit('change', {
|
||||
...this.form,
|
||||
commitMessage: this.form.commitMessage || this.defaultCommitMsg,
|
||||
branch:
|
||||
this.branchOption === this.$options.radioVals.NEW ? this.branchName : this.defaultBranch,
|
||||
});
|
||||
},
|
||||
focus(option) {
|
||||
if (option === this.$options.radioVals.NEW) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.branchName.$el.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<form @change="change">
|
||||
<p class="text-muted">
|
||||
{{
|
||||
s__(`Metrics|You can save a copy of this dashboard to your repository
|
||||
so it can be customized. Select a file name and branch to
|
||||
save it.`)
|
||||
}}
|
||||
</p>
|
||||
<gl-form-group
|
||||
ref="fileNameFormGroup"
|
||||
:label="__('File name')"
|
||||
:state="fileNameState"
|
||||
:invalid-feedback="fileNameFeedback"
|
||||
label-size="sm"
|
||||
label-for="fileName"
|
||||
>
|
||||
<gl-form-input id="fileName" ref="fileName" v-model="form.fileName" :required="true" />
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="__('Branch')" label-size="sm" label-for="branch">
|
||||
<gl-form-radio-group
|
||||
ref="branchOption"
|
||||
v-model="branchOption"
|
||||
:checked="$options.radioVals.NEW"
|
||||
:stacked="true"
|
||||
:options="branchOptions"
|
||||
@change="focus"
|
||||
/>
|
||||
<gl-form-input
|
||||
v-show="branchOption === $options.radioVals.NEW"
|
||||
id="branchName"
|
||||
ref="branchName"
|
||||
v-model="branchName"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
:label="__('Commit message (optional)')"
|
||||
label-size="sm"
|
||||
label-for="commitMessage"
|
||||
>
|
||||
<gl-form-textarea
|
||||
id="commitMessage"
|
||||
ref="commitMessage"
|
||||
v-model="form.commitMessage"
|
||||
:placeholder="defaultCommitMsg"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</form>
|
||||
</template>
|
|
@ -214,5 +214,29 @@ export const setPanelGroupMetrics = ({ commit }, data) => {
|
|||
commit(types.SET_PANEL_GROUP_METRICS, data);
|
||||
};
|
||||
|
||||
export const duplicateSystemDashboard = ({ state }, payload) => {
|
||||
const params = {
|
||||
dashboard: payload.dashboard,
|
||||
file_name: payload.fileName,
|
||||
branch: payload.branch,
|
||||
commit_message: payload.commitMessage,
|
||||
};
|
||||
|
||||
return axios
|
||||
.post(state.dashboardsEndpoint, params)
|
||||
.then(response => response.data)
|
||||
.then(data => data.dashboard)
|
||||
.catch(error => {
|
||||
const { response } = error;
|
||||
if (response && response.data && response.data.error) {
|
||||
throw sprintf(s__('Metrics|There was an error creating the dashboard. %{error}'), {
|
||||
error: response.data.error,
|
||||
});
|
||||
} else {
|
||||
throw s__('Metrics|There was an error creating the dashboard.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -175,6 +175,7 @@ export default {
|
|||
state.environmentsEndpoint = endpoints.environmentsEndpoint;
|
||||
state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
|
||||
state.dashboardEndpoint = endpoints.dashboardEndpoint;
|
||||
state.dashboardsEndpoint = endpoints.dashboardsEndpoint;
|
||||
state.currentDashboard = endpoints.currentDashboard;
|
||||
state.projectPath = endpoints.projectPath;
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import DeploymentInfo from './deployment_info.vue';
|
||||
import DeploymentViewButton from './deployment_view_button.vue';
|
||||
import DeploymentStopButton from './deployment_stop_button.vue';
|
||||
|
@ -14,9 +14,6 @@ export default {
|
|||
DeploymentStopButton,
|
||||
DeploymentViewButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
deployment: {
|
||||
type: Object,
|
||||
|
@ -43,6 +40,14 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
appButtonText() {
|
||||
return {
|
||||
text: this.isCurrent ? s__('Review App|View app') : s__('Review App|View latest app'),
|
||||
tooltip: this.isCurrent
|
||||
? ''
|
||||
: __('View the latest successful deployment to this environment'),
|
||||
};
|
||||
},
|
||||
canBeManuallyDeployed() {
|
||||
return this.computedDeploymentStatus === MANUAL_DEPLOY;
|
||||
},
|
||||
|
@ -55,9 +60,6 @@ export default {
|
|||
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;
|
||||
},
|
||||
|
@ -89,7 +91,7 @@ export default {
|
|||
<!-- show appropriate version of review app button -->
|
||||
<deployment-view-button
|
||||
v-if="hasExternalUrls"
|
||||
:is-current="isCurrent"
|
||||
:app-button-text="appButtonText"
|
||||
:deployment="deployment"
|
||||
:show-visual-review-app="showVisualReviewApp"
|
||||
:visual-review-app-metadata="visualReviewAppMeta"
|
||||
|
|
|
@ -11,12 +11,12 @@ export default {
|
|||
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
|
||||
},
|
||||
props: {
|
||||
deployment: {
|
||||
appButtonText: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isCurrent: {
|
||||
type: Boolean,
|
||||
deployment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showVisualReviewApp: {
|
||||
|
@ -60,7 +60,7 @@ export default {
|
|||
>
|
||||
<template slot="mainAction" slot-scope="slotProps">
|
||||
<review-app-link
|
||||
:is-current="isCurrent"
|
||||
:display="appButtonText"
|
||||
:link="deploymentExternalUrl"
|
||||
:css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
|
||||
/>
|
||||
|
@ -85,7 +85,7 @@ export default {
|
|||
</filtered-search-dropdown>
|
||||
<template v-else>
|
||||
<review-app-link
|
||||
:is-current="isCurrent"
|
||||
:display="appButtonText"
|
||||
:link="deploymentExternalUrl"
|
||||
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
|
||||
/>
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
cssClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isCurrent: {
|
||||
type: Boolean,
|
||||
display: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
link: {
|
||||
|
@ -20,15 +23,12 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
linkText() {
|
||||
return this.isCurrent ? __('View app') : __('View previous app');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
v-gl-tooltip
|
||||
:title="display.tooltip"
|
||||
:href="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
|
@ -36,6 +36,6 @@ export default {
|
|||
data-track-event="open_review_app"
|
||||
data-track-label="review_app"
|
||||
>
|
||||
{{ linkText }} <icon class="fgray" name="external-link" />
|
||||
{{ display.text }} <icon class="fgray" name="external-link" />
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -7,90 +7,53 @@ module Projects
|
|||
|
||||
before_action :check_repository_available!
|
||||
before_action :validate_required_params!
|
||||
before_action :validate_dashboard_template!
|
||||
before_action :authorize_push!
|
||||
|
||||
USER_DASHBOARDS_DIR = ::Metrics::Dashboard::ProjectDashboardService::DASHBOARD_ROOT
|
||||
DASHBOARD_TEMPLATES = {
|
||||
::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH => ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH
|
||||
}.freeze
|
||||
rescue_from ActionController::ParameterMissing do |exception|
|
||||
respond_error(http_status: :bad_request, message: _('Request parameter %{param} is missing.') % { param: exception.param })
|
||||
end
|
||||
|
||||
def create
|
||||
result = ::Files::CreateService.new(project, current_user, dashboard_attrs).execute
|
||||
result = ::Metrics::Dashboard::CloneDashboardService.new(project, current_user, dashboard_params).execute
|
||||
|
||||
if result[:status] == :success
|
||||
respond_success
|
||||
respond_success(result)
|
||||
else
|
||||
respond_error(result[:message])
|
||||
respond_error(result)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def respond_success
|
||||
def respond_success(result)
|
||||
set_web_ide_link_notice(result.dig(:dashboard, :path))
|
||||
respond_to do |format|
|
||||
format.html { redirect_to ide_edit_path(project, redirect_safe_branch_name, new_dashboard_path) }
|
||||
format.json { render json: { redirect_to: ide_edit_path(project, redirect_safe_branch_name, new_dashboard_path) }, status: :created }
|
||||
format.json { render status: result.delete(:http_status), json: result }
|
||||
end
|
||||
end
|
||||
|
||||
def respond_error(message)
|
||||
flash[:alert] = message
|
||||
|
||||
def respond_error(result)
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(default: namespace_project_environments_path) }
|
||||
format.json { render json: { error: message }, status: :bad_request }
|
||||
format.json { render json: { error: result[:message] }, status: result[:http_status] }
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_push!
|
||||
access_denied!(%q(You can't commit to this project)) unless user_access(project).can_push_to_branch?(params[:branch])
|
||||
def set_web_ide_link_notice(new_dashboard_path)
|
||||
web_ide_link_start = "<a href=\"#{ide_edit_path(project, redirect_safe_branch_name, new_dashboard_path)}\">"
|
||||
message = _("Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}.") % { web_ide_link_start: web_ide_link_start, web_ide_link_end: "</a>" }
|
||||
flash[:notice] = message.html_safe
|
||||
end
|
||||
|
||||
def validate_required_params!
|
||||
params.require(%i(branch file_name dashboard))
|
||||
end
|
||||
|
||||
def validate_dashboard_template!
|
||||
access_denied! unless dashboard_template
|
||||
end
|
||||
|
||||
def dashboard_attrs
|
||||
{
|
||||
commit_message: commit_message,
|
||||
file_path: new_dashboard_path,
|
||||
file_content: new_dashboard_content,
|
||||
encoding: 'text',
|
||||
branch_name: params[:branch],
|
||||
start_branch: repository.branch_exists?(params[:branch]) ? params[:branch] : project.default_branch
|
||||
}
|
||||
end
|
||||
|
||||
def commit_message
|
||||
params[:commit_message] || "Create custom dashboard #{params[:file_name]}"
|
||||
end
|
||||
|
||||
def new_dashboard_path
|
||||
File.join(USER_DASHBOARDS_DIR, params[:file_name])
|
||||
end
|
||||
|
||||
def new_dashboard_content
|
||||
File.read(Rails.root.join(dashboard_template))
|
||||
end
|
||||
|
||||
def dashboard_template
|
||||
dashboard_templates[params[:dashboard]]
|
||||
end
|
||||
|
||||
def dashboard_templates
|
||||
DASHBOARD_TEMPLATES
|
||||
params.require(%i(branch file_name dashboard commit_message))
|
||||
end
|
||||
|
||||
def redirect_safe_branch_name
|
||||
repository.find_branch(params[:branch]).name
|
||||
end
|
||||
|
||||
def dashboard_params
|
||||
params.permit(%i(branch file_name dashboard commit_message)).to_h
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Projects::PerformanceMonitoring::DashboardsController.prepend_if_ee('EE::Projects::PerformanceMonitoring::DashboardsController')
|
||||
|
|
23
app/graphql/resolvers/environments_resolver.rb
Normal file
23
app/graphql/resolvers/environments_resolver.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
class EnvironmentsResolver < BaseResolver
|
||||
argument :name, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'Name of the environment'
|
||||
|
||||
argument :search, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'Search query'
|
||||
|
||||
type Types::EnvironmentType, null: true
|
||||
|
||||
alias_method :project, :object
|
||||
|
||||
def resolve(**args)
|
||||
return unless project.present?
|
||||
|
||||
EnvironmentsFinder.new(project, context[:current_user], args).find
|
||||
end
|
||||
end
|
||||
end
|
16
app/graphql/types/environment_type.rb
Normal file
16
app/graphql/types/environment_type.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
class EnvironmentType < BaseObject
|
||||
graphql_name 'Environment'
|
||||
description 'Describes where code is deployed for a project'
|
||||
|
||||
authorize :read_environment
|
||||
|
||||
field :name, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Human-readable name of the environment'
|
||||
|
||||
field :id, GraphQL::ID_TYPE, null: false,
|
||||
description: 'ID of the environment'
|
||||
end
|
||||
end
|
|
@ -138,6 +138,12 @@ module Types
|
|||
description: 'Issues of the project',
|
||||
resolver: Resolvers::IssuesResolver
|
||||
|
||||
field :environments,
|
||||
Types::EnvironmentType.connection_type,
|
||||
null: true,
|
||||
description: 'Environments of the project',
|
||||
resolver: Resolvers::EnvironmentsResolver
|
||||
|
||||
field :issue,
|
||||
Types::IssueType,
|
||||
null: true,
|
||||
|
|
|
@ -29,8 +29,10 @@ module EnvironmentsHelper
|
|||
"empty-no-data-small-svg-path" => image_path('illustrations/chart-empty-state-small.svg'),
|
||||
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
|
||||
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
|
||||
"dashboards-endpoint" => project_performance_monitoring_dashboards_path(project, format: :json),
|
||||
"dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
|
||||
"deployments-endpoint" => project_environment_deployments_path(project, environment, format: :json),
|
||||
"default-branch" => project.default_branch,
|
||||
"environments-endpoint": project_environments_path(project, format: :json),
|
||||
"project-path" => project_path(project),
|
||||
"tags-path" => project_tags_path(project),
|
||||
|
|
|
@ -197,6 +197,10 @@ module Ci
|
|||
|
||||
AutoMergeProcessWorker.perform_async(merge_request.id)
|
||||
end
|
||||
|
||||
if pipeline.auto_devops_source?
|
||||
self.class.auto_devops_pipelines_completed_total.increment(status: pipeline.status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -330,6 +334,10 @@ module Ci
|
|||
::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource preparing pending]
|
||||
end
|
||||
|
||||
def self.auto_devops_pipelines_completed_total
|
||||
@auto_devops_pipelines_completed_total ||= Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines')
|
||||
end
|
||||
|
||||
def stages_count
|
||||
statuses.select(:stage).distinct.count
|
||||
end
|
||||
|
|
|
@ -307,6 +307,8 @@ class User < ApplicationRecord
|
|||
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
|
||||
scope :external, -> { where(external: true) }
|
||||
scope :active, -> { with_state(:active).non_internal }
|
||||
scope :active_without_ghosts, -> { with_state(:active).without_ghosts }
|
||||
scope :without_ghosts, -> { where('ghost IS NOT TRUE') }
|
||||
scope :deactivated, -> { with_state(:deactivated).non_internal }
|
||||
scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
|
||||
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
|
||||
|
@ -470,7 +472,7 @@ class User < ApplicationRecord
|
|||
when 'deactivated'
|
||||
deactivated
|
||||
else
|
||||
active
|
||||
active_without_ghosts
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -614,7 +616,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.non_internal
|
||||
where('ghost IS NOT TRUE')
|
||||
without_ghosts
|
||||
end
|
||||
|
||||
#
|
||||
|
|
113
app/services/metrics/dashboard/clone_dashboard_service.rb
Normal file
113
app/services/metrics/dashboard/clone_dashboard_service.rb
Normal file
|
@ -0,0 +1,113 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Copies system dashboard definition in .yml file into designated
|
||||
# .yml file inside `.gitlab/dashboards`
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class CloneDashboardService < ::BaseService
|
||||
ALLOWED_FILE_TYPE = '.yml'
|
||||
USER_DASHBOARDS_DIR = ::Metrics::Dashboard::ProjectDashboardService::DASHBOARD_ROOT
|
||||
|
||||
def self.allowed_dashboard_templates
|
||||
@allowed_dashboard_templates ||= Set[::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH].freeze
|
||||
end
|
||||
|
||||
def execute
|
||||
catch(:error) do
|
||||
throw(:error, error(_(%q(You can't commit to this project)), :forbidden)) unless push_authorized?
|
||||
|
||||
result = ::Files::CreateService.new(project, current_user, dashboard_attrs).execute
|
||||
throw(:error, wrap_error(result)) unless result[:status] == :success
|
||||
|
||||
repository.refresh_method_caches([:metrics_dashboard])
|
||||
success(result.merge(http_status: :created, dashboard: dashboard_details))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dashboard_attrs
|
||||
{
|
||||
commit_message: params[:commit_message],
|
||||
file_path: new_dashboard_path,
|
||||
file_content: new_dashboard_content,
|
||||
encoding: 'text',
|
||||
branch_name: branch,
|
||||
start_branch: repository.branch_exists?(branch) ? branch : project.default_branch
|
||||
}
|
||||
end
|
||||
|
||||
def dashboard_details
|
||||
{
|
||||
path: new_dashboard_path,
|
||||
display_name: ::Metrics::Dashboard::ProjectDashboardService.name_for_path(new_dashboard_path),
|
||||
default: false,
|
||||
system_dashboard: false
|
||||
}
|
||||
end
|
||||
|
||||
def push_authorized?
|
||||
Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
|
||||
end
|
||||
|
||||
def dashboard_template
|
||||
@dashboard_template ||= begin
|
||||
throw(:error, error(_('Not found.'), :not_found)) unless self.class.allowed_dashboard_templates.include?(params[:dashboard])
|
||||
|
||||
params[:dashboard]
|
||||
end
|
||||
end
|
||||
|
||||
def branch
|
||||
@branch ||= begin
|
||||
throw(:error, error(_('There was an error creating the dashboard, branch name is invalid.'), :bad_request)) unless valid_branch_name?
|
||||
throw(:error, error(_('There was an error creating the dashboard, branch named: %{branch} already exists.') % { branch: params[:branch] }, :bad_request)) unless new_or_default_branch? # temporary validation for first UI iteration
|
||||
|
||||
params[:branch]
|
||||
end
|
||||
end
|
||||
|
||||
def new_or_default_branch?
|
||||
!repository.branch_exists?(params[:branch]) || project.default_branch == params[:branch]
|
||||
end
|
||||
|
||||
def valid_branch_name?
|
||||
Gitlab::GitRefValidator.validate(params[:branch])
|
||||
end
|
||||
|
||||
def new_dashboard_path
|
||||
@new_dashboard_path ||= File.join(USER_DASHBOARDS_DIR, file_name)
|
||||
end
|
||||
|
||||
def file_name
|
||||
@file_name ||= begin
|
||||
throw(:error, error(_('The file name should have a .yml extension'), :bad_request)) unless target_file_type_valid?
|
||||
|
||||
File.basename(params[:file_name])
|
||||
end
|
||||
end
|
||||
|
||||
def target_file_type_valid?
|
||||
File.extname(params[:file_name]) == ALLOWED_FILE_TYPE
|
||||
end
|
||||
|
||||
def new_dashboard_content
|
||||
File.read(Rails.root.join(dashboard_template))
|
||||
end
|
||||
|
||||
def repository
|
||||
@repository ||= project.repository
|
||||
end
|
||||
|
||||
def wrap_error(result)
|
||||
if result[:message] == 'A file with this name already exists'
|
||||
error(_("A file with '%{file_name}' already exists in %{branch} branch") % { file_name: file_name, branch: branch }, :bad_request)
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Metrics::Dashboard::CloneDashboardService.prepend_if_ee('EE::Metrics::Dashboard::CloneDashboardService')
|
|
@ -9,7 +9,7 @@
|
|||
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
|
||||
= link_to admin_users_path do
|
||||
= s_('AdminUsers|Active')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.active)
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.active_without_ghosts)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
|
||||
= link_to admin_users_path(filter: "admins") do
|
||||
= s_('AdminUsers|Admins')
|
||||
|
|
|
@ -44,8 +44,10 @@
|
|||
= expanded ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
- auto_devops_url = help_page_path('topics/autodevops/index')
|
||||
- quickstart_url = help_page_path('topics/autodevops/quick_start_guide')
|
||||
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
|
||||
= s_('GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe }
|
||||
- quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
|
||||
= s_('AutoDevOps|Auto DevOps can automatically build, test, and deploy applications based on predefined continuous integration and delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end} or use our %{quickstart_start}quick start guide%{quickstart_end} to get started right away.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
|
||||
|
||||
.settings-content
|
||||
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
- if group_sidebar_link?(:contribution_analytics)
|
||||
= nav_link(path: 'analytics#show') do
|
||||
= link_to group_analytics_path(@group), title: _('Contribution Analytics'), data: { placement: 'right', qa_selector: 'contribution_analytics_link' } do
|
||||
= link_to group_contribution_analytics_path(@group), title: _('Contribution Analytics'), data: { placement: 'right', qa_selector: 'contribution_analytics_link' } do
|
||||
%span
|
||||
= _('Contribution Analytics')
|
||||
|
||||
|
|
|
@ -23,8 +23,11 @@
|
|||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
|
||||
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
|
||||
- auto_devops_url = help_page_path('topics/autodevops/index')
|
||||
- quickstart_url = help_page_path('topics/autodevops/quick_start_guide')
|
||||
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
|
||||
- quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
|
||||
= s_('AutoDevOps|Auto DevOps can automatically build, test, and deploy applications based on predefined continuous integration and delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end} or use our %{quickstart_start}quick start guide%{quickstart_end} to get started right away.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
|
||||
.settings-content
|
||||
= render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled?
|
||||
|
||||
|
|
5
changelogs/unreleased/196254-update-label-text.yml
Normal file
5
changelogs/unreleased/196254-update-label-text.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update button label in MR widget pipeline footer
|
||||
merge_request: 22900
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/30936-ado-quick-start.yml
Normal file
5
changelogs/unreleased/30936-ado-quick-start.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds quickstart doc link to ADO CICD settings
|
||||
merge_request:
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add ability to duplicate the common metrics dashboard
|
||||
merge_request: 21929
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Get Project's environment names via GraphQL
|
||||
merge_request: 22932
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Return empty body for 204 responses in API
|
||||
merge_request: 22086
|
||||
author:
|
||||
type: fixed
|
|
@ -38,9 +38,13 @@ rescue StandardError => e
|
|||
warn "There was a problem trying to check the Changelog. Exception: #{e.name} - #{e.message}"
|
||||
end
|
||||
|
||||
def sanitized_mr_title
|
||||
helper.sanitize_mr_title(gitlab.mr_json["title"])
|
||||
end
|
||||
|
||||
if git.modified_files.include?("CHANGELOG.md")
|
||||
fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" +
|
||||
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: changelog.sanitized_mr_title, labels: changelog.presented_no_changelog_labels)
|
||||
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title, labels: changelog.presented_no_changelog_labels)
|
||||
end
|
||||
|
||||
changelog_found = changelog.found
|
||||
|
@ -50,6 +54,6 @@ if changelog.needed?
|
|||
check_changelog(changelog_found)
|
||||
else
|
||||
message "**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html)**: If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.\n\n" +
|
||||
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: changelog.sanitized_mr_title, labels: changelog.presented_no_changelog_labels)
|
||||
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title, labels: changelog.presented_no_changelog_labels)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,295 +1,162 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
require_relative File.expand_path('../../lib/gitlab/danger/commit_linter', __dir__)
|
||||
|
||||
URL_LIMIT_SUBJECT = "https://chris.beams.io/posts/git-commit/#limit-50"
|
||||
URL_GIT_COMMIT = "https://chris.beams.io/posts/git-commit/"
|
||||
|
||||
# rubocop: disable Style/SignalException
|
||||
# rubocop: disable Metrics/CyclomaticComplexity
|
||||
# rubocop: disable Metrics/PerceivedComplexity
|
||||
|
||||
# Perform various checks against commits. We're not using
|
||||
# https://github.com/jonallured/danger-commit_lint because its output is not
|
||||
# very helpful, and it doesn't offer the means of ignoring merge commits.
|
||||
|
||||
class EmojiChecker
|
||||
DIGESTS = File.expand_path('../../fixtures/emojis/digests.json', __dir__)
|
||||
ALIASES = File.expand_path('../../fixtures/emojis/aliases.json', __dir__)
|
||||
|
||||
# A regex that indicates a piece of text _might_ include an Emoji. The regex
|
||||
# alone is not enough, as we'd match `:foo:bar:baz`. Instead, we use this
|
||||
# regex to save us from having to check for all possible emoji names when we
|
||||
# know one definitely is not included.
|
||||
LIKELY_EMOJI = /:[\+a-z0-9_\-]+:/.freeze
|
||||
|
||||
def initialize
|
||||
names = JSON.parse(File.read(DIGESTS)).keys +
|
||||
JSON.parse(File.read(ALIASES)).keys
|
||||
|
||||
@emoji = names.map { |name| ":#{name}:" }
|
||||
end
|
||||
|
||||
def includes_emoji?(text)
|
||||
return false unless text.match?(LIKELY_EMOJI)
|
||||
|
||||
@emoji.any? { |emoji| text.include?(emoji) }
|
||||
end
|
||||
end
|
||||
MAX_COMMITS_COUNT = 10
|
||||
|
||||
def gitlab_danger
|
||||
@gitlab_danger ||= GitlabDanger.new(helper.gitlab_helper)
|
||||
end
|
||||
|
||||
def fail_commit(commit, message)
|
||||
fail("#{commit.sha}: #{message}")
|
||||
self.fail("#{commit.sha}: #{message}")
|
||||
end
|
||||
|
||||
def warn_commit(commit, message)
|
||||
warn("#{commit.sha}: #{message}")
|
||||
self.warn("#{commit.sha}: #{message}")
|
||||
end
|
||||
|
||||
def lines_changed_in_commit(commit)
|
||||
commit.diff_parent.stats[:total][:lines]
|
||||
def squash_mr?
|
||||
gitlab_danger.ci? ? gitlab.mr_json['squash'] : false
|
||||
end
|
||||
|
||||
def subject_starts_with_capital?(subject)
|
||||
first_char = subject.chars.first
|
||||
|
||||
first_char.upcase == first_char
|
||||
def wip_mr?
|
||||
gitlab_danger.ci? ? gitlab.mr_json['work_in_progress'] : false
|
||||
end
|
||||
|
||||
def too_many_changed_lines?(commit)
|
||||
commit.diff_parent.stats[:total][:files] > 3 &&
|
||||
lines_changed_in_commit(commit) >= 30
|
||||
end
|
||||
# Perform various checks against commits. We're not using
|
||||
# https://github.com/jonallured/danger-commit_lint because its output is not
|
||||
# very helpful, and it doesn't offer the means of ignoring merge commits.
|
||||
def lint_commit(commit)
|
||||
linter = Gitlab::Danger::CommitLinter.new(commit)
|
||||
|
||||
def emoji_checker
|
||||
@emoji_checker ||= EmojiChecker.new
|
||||
end
|
||||
|
||||
def unicode_emoji_regex
|
||||
@unicode_emoji_regex ||= %r((
|
||||
[\u{1F300}-\u{1F5FF}] |
|
||||
[\u{1F1E6}-\u{1F1FF}] |
|
||||
[\u{2700}-\u{27BF}] |
|
||||
[\u{1F900}-\u{1F9FF}] |
|
||||
[\u{1F600}-\u{1F64F}] |
|
||||
[\u{1F680}-\u{1F6FF}] |
|
||||
[\u{2600}-\u{26FF}]
|
||||
))x
|
||||
end
|
||||
|
||||
def count_filtered_commits(commits)
|
||||
commits.count do |commit|
|
||||
!commit.message.start_with?('fixup!', 'squash!')
|
||||
end
|
||||
end
|
||||
|
||||
def lint_commit(commit) # rubocop:disable Metrics/AbcSize
|
||||
# For now we'll ignore merge commits, as getting rid of those is a problem
|
||||
# separate from enforcing good commit messages.
|
||||
return false if commit.message.start_with?('Merge branch')
|
||||
return linter if linter.merge?
|
||||
|
||||
# We ignore revert commits as they are well structured by Git already
|
||||
return false if commit.message.start_with?('Revert "')
|
||||
return linter if linter.revert?
|
||||
|
||||
is_squash = gitlab_danger.ci? ? gitlab.mr_json['squash'] : false
|
||||
is_wip = gitlab_danger.ci? ? gitlab.mr_json['work_in_progress'] : false
|
||||
is_fixup = commit.message.start_with?('fixup!', 'squash!')
|
||||
# If MR is set to squash, we ignore fixup commits
|
||||
return linter if linter.fixup? && squash_mr?
|
||||
|
||||
if is_fixup
|
||||
# The MR is set to squash - Danger adds an informative notice
|
||||
# The MR is not set to squash - Danger fails. if also WIP warn only, not error
|
||||
if is_squash
|
||||
return false
|
||||
end
|
||||
|
||||
if is_wip
|
||||
warn_commit(
|
||||
commit,
|
||||
'Squash or Fixup commits must be squashed before merge, or enable squash merge option'
|
||||
)
|
||||
if linter.fixup?
|
||||
msg = 'Squash or fixup commits must be squashed before merge, or enable squash merge option'
|
||||
if wip_mr? || squash_mr?
|
||||
warn_commit(commit, msg)
|
||||
else
|
||||
fail_commit(
|
||||
commit,
|
||||
'Squash or Fixup commits must be squashed before merge, or enable squash merge option'
|
||||
)
|
||||
fail_commit(commit, msg)
|
||||
end
|
||||
|
||||
# Makes no sense to process other rules for fixup commits, they trigger just more noise
|
||||
return false
|
||||
return linter
|
||||
end
|
||||
|
||||
# Fail if a suggestion commit is used and squash is not enabled
|
||||
if commit.message.start_with?('Apply suggestion to')
|
||||
if is_squash
|
||||
return false
|
||||
else
|
||||
fail_commit(
|
||||
commit,
|
||||
'If you are applying suggestions, enable squash in the merge request and re-run the failed job'
|
||||
)
|
||||
return true
|
||||
if linter.suggestion?
|
||||
unless squash_mr?
|
||||
fail_commit(commit, "If you are applying suggestions, enable squash in the merge request and re-run the `danger-review` job")
|
||||
end
|
||||
|
||||
return linter
|
||||
end
|
||||
|
||||
failures = false
|
||||
subject, separator, details = commit.message.split("\n", 3)
|
||||
linter.lint
|
||||
end
|
||||
|
||||
if subject.split.length < 3
|
||||
fail_commit(
|
||||
commit,
|
||||
'The commit subject must contain at least three words'
|
||||
)
|
||||
def lint_mr_title(mr_title)
|
||||
commit = Struct.new(:message, :sha).new(mr_title)
|
||||
|
||||
failures = true
|
||||
end
|
||||
Gitlab::Danger::CommitLinter.new(commit).lint_subject("merge request title")
|
||||
end
|
||||
|
||||
if subject.length > 72
|
||||
fail_commit(
|
||||
commit,
|
||||
'The commit subject may not be longer than 72 characters'
|
||||
)
|
||||
|
||||
failures = true
|
||||
elsif subject.length > 50
|
||||
warn_commit(
|
||||
commit,
|
||||
"This commit's subject line is acceptable, but please try to [reduce it to 50 characters](#{URL_LIMIT_SUBJECT})."
|
||||
)
|
||||
end
|
||||
|
||||
unless subject_starts_with_capital?(subject)
|
||||
fail_commit(commit, 'The commit subject must start with a capital letter')
|
||||
failures = true
|
||||
end
|
||||
|
||||
if subject.end_with?('.')
|
||||
fail_commit(commit, 'The commit subject must not end with a period')
|
||||
failures = true
|
||||
end
|
||||
|
||||
if separator && !separator.empty?
|
||||
fail_commit(
|
||||
commit,
|
||||
'The commit subject and body must be separated by a blank line'
|
||||
)
|
||||
|
||||
failures = true
|
||||
end
|
||||
|
||||
details&.each_line do |line|
|
||||
line = line.strip
|
||||
|
||||
next if line.length <= 72
|
||||
|
||||
url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length }
|
||||
|
||||
# If the line includes a URL, we'll allow it to exceed 72 characters, but
|
||||
# only if the line _without_ the URL does not exceed this limit.
|
||||
next if line.length - url_size <= 72
|
||||
|
||||
fail_commit(
|
||||
commit,
|
||||
'The commit body should not contain more than 72 characters per line'
|
||||
)
|
||||
|
||||
failures = true
|
||||
end
|
||||
|
||||
if !details && too_many_changed_lines?(commit)
|
||||
fail_commit(
|
||||
commit,
|
||||
'Commits that change 30 or more lines across at least three files ' \
|
||||
'must describe these changes in the commit body'
|
||||
)
|
||||
|
||||
failures = true
|
||||
end
|
||||
|
||||
if emoji_checker.includes_emoji?(commit.message)
|
||||
warn_commit(
|
||||
commit,
|
||||
'Avoid the use of Markdown Emoji such as `:+1:`. ' \
|
||||
'These add limited value to the commit message, ' \
|
||||
'and are displayed as plain text outside of GitLab'
|
||||
)
|
||||
|
||||
failures = true
|
||||
end
|
||||
|
||||
if commit.message.match?(unicode_emoji_regex)
|
||||
fail_commit(
|
||||
commit,
|
||||
'Avoid the use of Unicode Emoji. ' \
|
||||
'These add no value to the commit message, ' \
|
||||
'and may not be displayed properly everywhere'
|
||||
)
|
||||
|
||||
failures = true
|
||||
end
|
||||
|
||||
if commit.message.match?(%r(([\w\-\/]+)?(#|!|&|%)\d+\b))
|
||||
fail_commit(
|
||||
commit,
|
||||
'Use full URLs instead of short references ' \
|
||||
'(`gitlab-org/gitlab#123` or `!123`), as short references are ' \
|
||||
'displayed as plain text outside of GitLab'
|
||||
)
|
||||
|
||||
failures = true
|
||||
end
|
||||
|
||||
failures
|
||||
def count_non_fixup_commits(commit_linters)
|
||||
commit_linters.count { |commit_linter| !commit_linter.fixup? }
|
||||
end
|
||||
|
||||
def lint_commits(commits)
|
||||
failed = commits.select do |commit|
|
||||
lint_commit(commit)
|
||||
commit_linters = commits.map { |commit| lint_commit(commit) }
|
||||
failed_commit_linters = commit_linters.select { |commit_linter| commit_linter.failed? }
|
||||
warn_or_fail_commits(failed_commit_linters, default_to_fail: !squash_mr?)
|
||||
|
||||
if count_non_fixup_commits(commit_linters) > MAX_COMMITS_COUNT
|
||||
level = squash_mr? ? :warn : :fail
|
||||
self.__send__(level, # rubocop:disable GitlabSecurity/PublicSend
|
||||
"This merge request includes more than #{MAX_COMMITS_COUNT} commits. " \
|
||||
'Please rebase these commits into a smaller number of commits or split ' \
|
||||
'this merge request into multiple smaller merge requests.')
|
||||
end
|
||||
|
||||
if failed.any?
|
||||
markdown(<<~MARKDOWN)
|
||||
## Commit message standards
|
||||
if squash_mr?
|
||||
multi_line_commit_linter = commit_linters.detect { |commit_linter| commit_linter.multi_line? }
|
||||
|
||||
One or more commit messages do not meet our Git commit message standards.
|
||||
For more information on how to write a good commit message, take a look at
|
||||
[How to Write a Git Commit Message](#{URL_GIT_COMMIT}).
|
||||
|
||||
Here is an example of a good commit message:
|
||||
|
||||
Reject ruby interpolation in externalized strings
|
||||
|
||||
When using ruby interpolation in externalized strings, they can't be
|
||||
detected. Which means they will never be presented to be translated.
|
||||
|
||||
To mix variables into translations we need to use `sprintf`
|
||||
instead.
|
||||
|
||||
Instead of:
|
||||
|
||||
_("Hello \#{subject}")
|
||||
|
||||
Use:
|
||||
|
||||
_("Hello %{subject}") % { subject: 'world' }
|
||||
|
||||
This is an example of a bad commit message:
|
||||
|
||||
updated README.md
|
||||
|
||||
This commit message is bad because although it tells us that README.md is
|
||||
updated, it doesn't tell us why or how it was updated.
|
||||
MARKDOWN
|
||||
if multi_line_commit_linter && multi_line_commit_linter.lint.failed?
|
||||
warn_or_fail_commits(multi_line_commit_linter)
|
||||
fail_message('The commit message that will be used in the squash commit does not meet our Git commit message standards.')
|
||||
else
|
||||
title_linter = lint_mr_title(gitlab.mr_json['title'])
|
||||
if title_linter.failed?
|
||||
warn_or_fail_commits(title_linter)
|
||||
fail_message('The merge request title that will be used in the squash commit does not meet our Git commit message standards.')
|
||||
end
|
||||
end
|
||||
else
|
||||
if failed_commit_linters.any?
|
||||
fail_message('One or more commit messages do not meet our Git commit message standards.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def warn_or_fail_commits(failed_linters, default_to_fail: true)
|
||||
level = default_to_fail ? :fail : :warn
|
||||
|
||||
Array(failed_linters).each do |linter|
|
||||
linter.problems.each do |problem_key, problem_desc|
|
||||
case problem_key
|
||||
when :subject_above_warning
|
||||
warn_commit(linter.commit, problem_desc)
|
||||
else
|
||||
self.__send__("#{level}_commit", linter.commit, problem_desc) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fail_message(intro)
|
||||
markdown(<<~MARKDOWN)
|
||||
## Commit message standards
|
||||
|
||||
#{intro}
|
||||
|
||||
For more information on how to write a good commit message, take a look at
|
||||
[How to Write a Git Commit Message](#{URL_GIT_COMMIT}).
|
||||
|
||||
Here is an example of a good commit message:
|
||||
|
||||
Reject ruby interpolation in externalized strings
|
||||
|
||||
When using ruby interpolation in externalized strings, they can't be
|
||||
detected. Which means they will never be presented to be translated.
|
||||
|
||||
To mix variables into translations we need to use `sprintf`
|
||||
instead.
|
||||
|
||||
Instead of:
|
||||
|
||||
_("Hello \#{subject}")
|
||||
|
||||
Use:
|
||||
|
||||
_("Hello %{subject}") % { subject: 'world' }
|
||||
|
||||
This is an example of a bad commit message:
|
||||
|
||||
updated README.md
|
||||
|
||||
This commit message is bad because although it tells us that README.md is
|
||||
updated, it doesn't tell us why or how it was updated.
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
lint_commits(git.commits)
|
||||
|
||||
if count_filtered_commits(git.commits) > 10
|
||||
fail(
|
||||
'This merge request includes more than 10 commits. ' \
|
||||
'Please rebase these commits into a smaller number of commits.'
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1412,6 +1412,56 @@ enum EntryType {
|
|||
tree
|
||||
}
|
||||
|
||||
"""
|
||||
Describes where code is deployed for a project
|
||||
"""
|
||||
type Environment {
|
||||
"""
|
||||
ID of the environment
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Human-readable name of the environment
|
||||
"""
|
||||
name: String!
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for Environment.
|
||||
"""
|
||||
type EnvironmentConnection {
|
||||
"""
|
||||
A list of edges.
|
||||
"""
|
||||
edges: [EnvironmentEdge]
|
||||
|
||||
"""
|
||||
A list of nodes.
|
||||
"""
|
||||
nodes: [Environment]
|
||||
|
||||
"""
|
||||
Information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
type EnvironmentEdge {
|
||||
"""
|
||||
A cursor for use in pagination.
|
||||
"""
|
||||
cursor: String!
|
||||
|
||||
"""
|
||||
The item at the end of the edge.
|
||||
"""
|
||||
node: Environment
|
||||
}
|
||||
|
||||
"""
|
||||
Represents an epic.
|
||||
"""
|
||||
|
@ -4706,6 +4756,41 @@ type Project {
|
|||
"""
|
||||
descriptionHtml: String
|
||||
|
||||
"""
|
||||
Environments of the project
|
||||
"""
|
||||
environments(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
|
||||
"""
|
||||
Name of the environment
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
Search query
|
||||
"""
|
||||
search: String
|
||||
): EnvironmentConnection
|
||||
|
||||
"""
|
||||
Number of times the project has been forked
|
||||
"""
|
||||
|
|
|
@ -406,6 +406,79 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "environments",
|
||||
"description": "Environments of the project",
|
||||
"args": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Name of the environment",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"description": "Search query",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "EnvironmentConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "forksCount",
|
||||
"description": "Number of times the project has been forked",
|
||||
|
@ -15431,6 +15504,167 @@
|
|||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "EnvironmentConnection",
|
||||
"description": "The connection type for Environment.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "EnvironmentEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Environment",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "EnvironmentEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Environment",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Environment",
|
||||
"description": "Describes where code is deployed for a project",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "ID of the environment",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Human-readable name of the environment",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryDetailedError",
|
||||
|
|
|
@ -232,6 +232,15 @@ Autogenerated return type of DestroySnippet
|
|||
| `replyId` | ID! | ID used to reply to this discussion |
|
||||
| `createdAt` | Time! | Timestamp of the discussion's creation |
|
||||
|
||||
## Environment
|
||||
|
||||
Describes where code is deployed for a project
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `name` | String! | Human-readable name of the environment |
|
||||
| `id` | ID! | ID of the environment |
|
||||
|
||||
## Epic
|
||||
|
||||
Represents an epic.
|
||||
|
|
|
@ -6,7 +6,7 @@ Configuration for approvals on all Merge Requests (MR) in the project. Must be a
|
|||
|
||||
### Get Configuration
|
||||
|
||||
>**Note:** This API endpoint is only available on 10.6 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/183) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
|
||||
|
||||
You can request information about a project's approval configuration using the
|
||||
following endpoint:
|
||||
|
@ -31,7 +31,7 @@ GET /projects/:id/approvals
|
|||
|
||||
### Change configuration
|
||||
|
||||
>**Note:** This API endpoint is only available on 10.6 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/183) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
|
||||
|
||||
If you are allowed to, you can change approval configuration using the following
|
||||
endpoint:
|
||||
|
@ -63,7 +63,7 @@ POST /projects/:id/approvals
|
|||
|
||||
### Get project-level rules
|
||||
|
||||
>**Note:** This API endpoint is only available on 12.3 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11877) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
||||
You can request information about a project's approval rules using the following endpoint:
|
||||
|
||||
|
@ -137,7 +137,7 @@ GET /projects/:id/approval_rules
|
|||
|
||||
### Create project-level rule
|
||||
|
||||
>**Note:** This API endpoint is only available on 12.3 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11877) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
||||
You can create project approval rules using the following endpoint:
|
||||
|
||||
|
@ -213,7 +213,7 @@ POST /projects/:id/approval_rules
|
|||
|
||||
### Update project-level rule
|
||||
|
||||
>**Note:** This API endpoint is only available on 12.3 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11877) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
||||
You can update project approval rules using the following endpoint:
|
||||
|
||||
|
@ -292,7 +292,7 @@ PUT /projects/:id/approval_rules/:approval_rule_id
|
|||
|
||||
### Delete project-level rule
|
||||
|
||||
>**Note:** This API endpoint is only available on 12.3 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11877) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
||||
You can delete project approval rules using the following endpoint:
|
||||
|
||||
|
@ -310,7 +310,7 @@ DELETE /projects/:id/approval_rules/:approval_rule_id
|
|||
### Change allowed approvers
|
||||
|
||||
>**Note:** This API endpoint has been deprecated. Please use Approval Rule API instead.
|
||||
>**Note:** This API endpoint is only available on 10.6 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/183) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
|
||||
|
||||
If you are allowed to, you can change approvers and approver groups using
|
||||
the following endpoint:
|
||||
|
@ -373,7 +373,7 @@ Configuration for approvals on a specific Merge Request. Must be authenticated f
|
|||
|
||||
### Get Configuration
|
||||
|
||||
>**Note:** This API endpoint is only available on 8.9 Starter and above.
|
||||
> Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 8.9.
|
||||
|
||||
You can request information about a merge request's approval status using the
|
||||
following endpoint:
|
||||
|
@ -419,7 +419,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/approvals
|
|||
|
||||
### Change approval configuration
|
||||
|
||||
>**Note:** This API endpoint is only available on 10.6 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/183) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
|
||||
|
||||
If you are allowed to, you can change `approvals_required` using the following
|
||||
endpoint:
|
||||
|
@ -456,7 +456,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/approvals
|
|||
### Change allowed approvers for Merge Request
|
||||
|
||||
>**Note:** This API endpoint has been deprecated. Please use Approval Rule API instead.
|
||||
>**Note:** This API endpoint is only available on 10.6 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/183) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
|
||||
|
||||
If you are allowed to, you can change approvers and approver groups using
|
||||
the following endpoint:
|
||||
|
@ -598,7 +598,7 @@ This includes additional information about the users who have already approved
|
|||
|
||||
### Get merge request level rules
|
||||
|
||||
>**Note:** This API endpoint is only available on 12.3 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13712) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
||||
You can request information about a merge request's approval rules using the following endpoint:
|
||||
|
||||
|
@ -674,7 +674,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/approval_rules
|
|||
|
||||
### Create merge request level rule
|
||||
|
||||
>**Note:** This API endpoint is only available on 12.3 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11877) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
||||
You can create merge request approval rules using the following endpoint:
|
||||
|
||||
|
@ -757,7 +757,7 @@ will be used.
|
|||
|
||||
### Update merge request level rule
|
||||
|
||||
>**Note:** This API endpoint is only available on 12.3 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11877) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
||||
You can update merge request approval rules using the following endpoint:
|
||||
|
||||
|
@ -841,7 +841,7 @@ These are system generated rules.
|
|||
|
||||
### Delete merge request level rule
|
||||
|
||||
>**Note:** This API endpoint is only available on 12.3 Starter and above.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11877) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
||||
You can delete merge request approval rules using the following endpoint:
|
||||
|
||||
|
@ -862,7 +862,7 @@ These are system generated rules.
|
|||
|
||||
## Approve Merge Request
|
||||
|
||||
>**Note:** This API endpoint is only available on 8.9 Starter and above.
|
||||
> Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 8.9.
|
||||
|
||||
If you are allowed to, you can approve a merge request using the following
|
||||
endpoint:
|
||||
|
@ -925,7 +925,7 @@ does not match, the response code will be `409`.
|
|||
|
||||
## Unapprove Merge Request
|
||||
|
||||
>**Note:** This API endpoint is only available on 9.0 Starter and above.
|
||||
>Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 9.0.
|
||||
|
||||
If you did approve a merge request, you can unapprove it using the following
|
||||
endpoint:
|
||||
|
|
|
@ -92,6 +92,12 @@ For instance:
|
|||
Model.create(foo: params[:foo])
|
||||
```
|
||||
|
||||
## Using HTTP status helpers
|
||||
|
||||
For non-200 HTTP responses, use the provided helpers in `lib/api/helpers.rb` to ensure correct behaviour (`not_found!`, `no_content!` etc.). These will `throw` inside Grape and abort the execution of your endpoint.
|
||||
|
||||
For `DELETE` requests, you should also generally use the `destroy_conditionally!` helper which by default returns a `204 No Content` response on success, or a `412 Precondition Failed` response if the given `If-Unmodified-Since` header is out of range. This helper calls `#destroy` on the passed resource, but you can also implement a custom deletion method by passing a block.
|
||||
|
||||
## Using API path helpers in GitLab Rails codebase
|
||||
|
||||
Because we support [installing GitLab under a relative URL], one must take this
|
||||
|
|
|
@ -941,7 +941,7 @@ a helpful link back to how the feature was developed.
|
|||
Over time, version text will reference a progressively older version of GitLab. In cases where version text
|
||||
refers to versions of GitLab four or more major versions back, consider removing the text.
|
||||
|
||||
For example, if the current major version is 11.x, version text referencing versions of GitLab 7.x
|
||||
For example, if the current major version is 12.x, version text referencing versions of GitLab 8.x
|
||||
and older are candidates for removal.
|
||||
|
||||
NOTE: **Note:**
|
||||
|
|
|
@ -78,3 +78,71 @@ follow up issue and attach it to the component implementation epic found within
|
|||
|
||||
If you are using a submit button inside a form and you attach an `onSubmit` event listener on the form element, [this piece of code](https://gitlab.com/gitlab-org/gitlab/blob/794c247a910e2759ce9b401356432a38a4535d49/app/assets/javascripts/main.js#L225) will add a `disabled` class selector to the submit button when the form is submitted.
|
||||
To avoid this behavior, add the class `js-no-auto-disable` to the button.
|
||||
|
||||
### 5. Should I use a full URL (i.e. `gon.gitlab_url`) or a full path (i.e. `gon.relative_url_root`) when referencing backend endpoints?
|
||||
|
||||
It's preferred to use a **full path** over a **full URL** because the URL will use the hostname configured with
|
||||
GitLab which may not match the request. This will cause [CORS issues like this Web IDE one](https://gitlab.com/gitlab-org/gitlab/issues/36810).
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
// bad :(
|
||||
// If gitlab is configured with hostname `0.0.0.0`
|
||||
// This will cause CORS issues if I request from `localhost`
|
||||
axios.get(joinPaths(gon.gitlab_url, '-', 'foo'))
|
||||
|
||||
// good :)
|
||||
axios.get(joinPaths(gon.relative_url_root, '-', 'foo'))
|
||||
```
|
||||
|
||||
Also, please try not to hardcode paths in the Frontend, but instead receive them from the Backend (see next section).
|
||||
When referencing Backend rails paths, avoid using `*_url`, and use `*_path` instead.
|
||||
|
||||
Example:
|
||||
|
||||
```haml
|
||||
-# Bad :(
|
||||
#js-foo{ data: { foo_url: some_rails_foo_url } }
|
||||
|
||||
-# Good :)
|
||||
#js-foo{ data: { foo_path: some_rails_foo_path } }
|
||||
```
|
||||
|
||||
### 6. How should the Frontend reference Backend paths?
|
||||
|
||||
We prefer not to add extra coupling by hardcoding paths. If possible,
|
||||
add these paths as data attributes to the DOM element being referenced in the JavaScript.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
// Bad :(
|
||||
// Here's a Vuex action that hardcodes a path :(
|
||||
export const fetchFoos = ({ state }) => {
|
||||
return axios.get(joinPaths(gon.relative_url_root, '-', 'foo'));
|
||||
};
|
||||
|
||||
// Good :)
|
||||
function initFoo() {
|
||||
const el = document.getElementById('js-foo');
|
||||
|
||||
// Path comes from our root element's data which is used to initialize the store :)
|
||||
const store = createStore({
|
||||
fooPath: el.dataset.fooPath
|
||||
});
|
||||
|
||||
Vue.extend({
|
||||
store,
|
||||
el,
|
||||
render(h) {
|
||||
return h(Component);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Vuex action can now reference the path from it's state :)
|
||||
export const fetchFoos = ({ state }) => {
|
||||
return axios.get(state.settings.fooPath);
|
||||
};
|
||||
```
|
||||
|
|
|
@ -433,6 +433,8 @@ Filebeat will run as a DaemonSet on each node in your cluster, and it will ship
|
|||
GitLab will then connect to Elasticsearch for logs instead of the Kubernetes API,
|
||||
and you will have access to more advanced querying capabilities.
|
||||
|
||||
Log data is automatically deleted after 15 days using [Curator](https://www.elastic.co/guide/en/elasticsearch/client/curator/5.5/about.html).
|
||||
|
||||
This is a preliminary release of Elastic Stack as a GitLab-managed application. By default,
|
||||
the ability to install it is disabled.
|
||||
|
||||
|
|
|
@ -155,12 +155,13 @@ NOTE: **Note:**
|
|||
The custom metrics as defined below do not support alerts, unlike
|
||||
[additional metrics](#adding-additional-metrics-premium).
|
||||
|
||||
Dashboards have several components:
|
||||
#### Adding a new dashboard to your project
|
||||
|
||||
- Panel groups, which comprise panels.
|
||||
- Panels, which support one or more metrics.
|
||||
You can configure a custom dashboard by adding a new `.yml` file into a project's repository. Only `.yml` files present in the projects **default** branch are displayed on the project's **Operations > Metrics** section.
|
||||
|
||||
To configure a custom dashboard:
|
||||
You may create a new file from scratch or duplicate a GitLab-defined dashboard.
|
||||
|
||||
**Add a `.yml` file manually**
|
||||
|
||||
1. Create a YAML file with the `.yml` extension under your repository's root
|
||||
directory inside `.gitlab/dashboards/`. For example, create
|
||||
|
@ -185,7 +186,7 @@ To configure a custom dashboard:
|
|||
define the layout of the dashboard and the Prometheus queries used to populate
|
||||
data.
|
||||
|
||||
1. Save the file, commit, and push to your repository.
|
||||
1. Save the file, commit, and push to your repository. The file must be present in your **default** branch.
|
||||
1. Navigate to your project's **Operations > Metrics** and choose the custom
|
||||
dashboard from the dropdown.
|
||||
|
||||
|
@ -193,6 +194,28 @@ NOTE: **Note:**
|
|||
Configuration files nested under subdirectories of `.gitlab/dashboards` are not
|
||||
supported and will not be available in the UI.
|
||||
|
||||
**Duplicate a GitLab-defined dashboard as a new `.yml` file**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/37238) in GitLab 12.7.
|
||||
|
||||
You can save a copy of a GitLab defined dashboard that can be customized and adapted to your project. You can decide to save the dashboard new `.yml` file in the project's **default** branch or in a newly created branch with a name of your choosing.
|
||||
|
||||
1. Click on the "Duplicate dashboard" in the dashboard dropdown.
|
||||
|
||||
NOTE:**Note:**
|
||||
Only GitLab-defined dashboards can be duplicated.
|
||||
|
||||
1. Input the file name and other information, such as a new commit message, and click on "Duplicate".
|
||||
|
||||
If you select your **default** branch, the new dashboard will become immediately available. If you select another branch, this branch should be merged to your **default** branch first.
|
||||
|
||||
#### Dashboard YAML properties
|
||||
|
||||
Dashboards have several components:
|
||||
|
||||
- Panel groups, which comprise of panels.
|
||||
- Panels, which support one or more metrics.
|
||||
|
||||
The following tables outline the details of expected properties.
|
||||
|
||||
**Dashboard properties:**
|
||||
|
|
|
@ -41,6 +41,10 @@ CAUTION: **CAUTION:**
|
|||
From GitLab 12.6 onwards, if the [visibility of an upstream project is reduced](../../../public_access/public_access.md#reducing-visibility)
|
||||
in any way, the fork relationship with all its forks will be removed.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
[Repository mirroring](repository_mirroring.md) will help to keep your fork synced with the original repository.
|
||||
Before approving a merge request you'll likely to be asked to sync before getting approval, hence automating it is recommend.
|
||||
|
||||
## Merging upstream
|
||||
|
||||
Once you are ready to send your code back to the main project, you need
|
||||
|
|
|
@ -38,7 +38,7 @@ module API
|
|||
application = ApplicationsFinder.new(params).execute
|
||||
application.destroy
|
||||
|
||||
status 204
|
||||
no_content!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -135,7 +135,6 @@ module API
|
|||
end
|
||||
|
||||
destroy_conditionally!(badge)
|
||||
body false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,7 +57,7 @@ module API
|
|||
requires :branch, type: String, desc: 'The name of the branch'
|
||||
end
|
||||
head do
|
||||
user_project.repository.branch_exists?(params[:branch]) ? status(204) : status(404)
|
||||
user_project.repository.branch_exists?(params[:branch]) ? no_content! : not_found!
|
||||
end
|
||||
get do
|
||||
branch = find_branch!(params[:branch])
|
||||
|
|
|
@ -77,7 +77,7 @@ module API
|
|||
|
||||
resource.custom_attributes.find_by!(key: params[:key]).destroy
|
||||
|
||||
status 204
|
||||
no_content!
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
|
|
@ -74,7 +74,7 @@ module API
|
|||
delete ':name' do
|
||||
Feature.get(params[:name]).remove
|
||||
|
||||
status 204
|
||||
no_content!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,7 +67,7 @@ module API
|
|||
milestone = user_group.milestones.find(params[:milestone_id])
|
||||
Milestones::DestroyService.new(user_group, current_user).execute(milestone)
|
||||
|
||||
status(204)
|
||||
no_content!
|
||||
end
|
||||
|
||||
desc 'Get all issues for a single group milestone' do
|
||||
|
|
|
@ -31,6 +31,7 @@ module API
|
|||
check_unmodified_since!(last_updated)
|
||||
|
||||
status 204
|
||||
body false
|
||||
|
||||
if block_given?
|
||||
yield resource
|
||||
|
|
|
@ -17,9 +17,9 @@ module API
|
|||
delete ':id/pages' do
|
||||
authorize! :remove_pages, user_project
|
||||
|
||||
status 204
|
||||
|
||||
::Pages::DeleteService.new(user_project, current_user).execute
|
||||
|
||||
no_content!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -148,8 +148,9 @@ module API
|
|||
delete ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
|
||||
authorize! :update_pages, user_project
|
||||
|
||||
status 204
|
||||
pages_domain.destroy
|
||||
|
||||
no_content!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,7 +69,7 @@ module API
|
|||
milestone = user_project.milestones.find(params[:milestone_id])
|
||||
Milestones::DestroyService.new(user_project, current_user).execute(milestone)
|
||||
|
||||
status(204)
|
||||
no_content!
|
||||
end
|
||||
|
||||
desc 'Get all issues for a single project milestone' do
|
||||
|
|
|
@ -447,7 +447,7 @@ module API
|
|||
::Projects::UnlinkForkService.new(user_project, current_user).execute
|
||||
end
|
||||
|
||||
result ? status(204) : not_modified!
|
||||
not_modified! unless result
|
||||
end
|
||||
|
||||
desc 'Share the project with a group' do
|
||||
|
|
|
@ -346,8 +346,9 @@ module API
|
|||
key = user.gpg_keys.find_by(id: params[:key_id])
|
||||
not_found!('GPG Key') unless key
|
||||
|
||||
status 204
|
||||
key.destroy
|
||||
|
||||
no_content!
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
@ -760,8 +761,9 @@ module API
|
|||
key = current_user.gpg_keys.find_by(id: params[:key_id])
|
||||
not_found!('GPG Key') unless key
|
||||
|
||||
status 204
|
||||
key.destroy
|
||||
|
||||
no_content!
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -111,9 +111,10 @@ module API
|
|||
variable = user_project.variables.find_by(key: params[:key])
|
||||
not_found!('Variable') unless variable
|
||||
|
||||
# Variables don't have any timestamp. Therfore, destroy unconditionally.
|
||||
status 204
|
||||
# Variables don't have a timestamp. Therefore, destroy unconditionally.
|
||||
variable.destroy
|
||||
|
||||
no_content!
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
|
|
@ -107,8 +107,9 @@ module API
|
|||
delete ':id/wikis/:slug' do
|
||||
authorize! :admin_wiki, user_project
|
||||
|
||||
status 204
|
||||
WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page)
|
||||
|
||||
no_content!
|
||||
end
|
||||
|
||||
desc 'Upload an attachment to the wiki repository' do
|
||||
|
|
232
lib/gitlab/danger/commit_linter.rb
Normal file
232
lib/gitlab/danger/commit_linter.rb
Normal file
|
@ -0,0 +1,232 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
emoji_checker_path = File.expand_path('emoji_checker', __dir__)
|
||||
defined?(Rails) ? require_dependency(emoji_checker_path) : require_relative(emoji_checker_path)
|
||||
|
||||
module Gitlab
|
||||
module Danger
|
||||
class CommitLinter
|
||||
MIN_SUBJECT_WORDS_COUNT = 3
|
||||
MAX_LINE_LENGTH = 72
|
||||
WARN_SUBJECT_LENGTH = 50
|
||||
URL_LIMIT_SUBJECT = "https://chris.beams.io/posts/git-commit/#limit-50"
|
||||
MAX_CHANGED_FILES_IN_COMMIT = 3
|
||||
MAX_CHANGED_LINES_IN_COMMIT = 30
|
||||
SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(#|!|&|%)\d+\b}.freeze
|
||||
DEFAULT_SUBJECT_DESCRIPTION = 'commit subject'
|
||||
PROBLEMS = {
|
||||
subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
|
||||
subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
|
||||
subject_above_warning: "The %s length is acceptable, but please try to [reduce it to #{WARN_SUBJECT_LENGTH} characters](#{URL_LIMIT_SUBJECT}).",
|
||||
subject_starts_with_lowercase: "The %s must start with a capital letter",
|
||||
subject_ends_with_a_period: "The %s must not end with a period",
|
||||
separator_missing: "The commit subject and body must be separated by a blank line",
|
||||
details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
|
||||
"at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
|
||||
details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
|
||||
message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
|
||||
"to the commit message, and are displayed as plain text outside of GitLab",
|
||||
message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
|
||||
"message, and may not be displayed properly everywhere",
|
||||
message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
|
||||
"`!123`), as short references are displayed as plain text outside of GitLab"
|
||||
}.freeze
|
||||
|
||||
attr_reader :commit, :problems
|
||||
|
||||
def initialize(commit)
|
||||
@commit = commit
|
||||
@problems = {}
|
||||
@linted = false
|
||||
end
|
||||
|
||||
def fixup?
|
||||
commit.message.start_with?('fixup!', 'squash!')
|
||||
end
|
||||
|
||||
def suggestion?
|
||||
commit.message.start_with?('Apply suggestion to')
|
||||
end
|
||||
|
||||
def merge?
|
||||
commit.message.start_with?('Merge branch')
|
||||
end
|
||||
|
||||
def revert?
|
||||
commit.message.start_with?('Revert "')
|
||||
end
|
||||
|
||||
def multi_line?
|
||||
!details.nil? && !details.empty?
|
||||
end
|
||||
|
||||
def failed?
|
||||
problems.any?
|
||||
end
|
||||
|
||||
def add_problem(problem_key, *args)
|
||||
@problems[problem_key] = sprintf(PROBLEMS[problem_key], *args)
|
||||
end
|
||||
|
||||
def lint(subject_description = "commit subject")
|
||||
return self if @linted
|
||||
|
||||
@linted = true
|
||||
lint_subject(subject_description)
|
||||
lint_separator
|
||||
lint_details
|
||||
lint_message
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def lint_subject(subject_description)
|
||||
if subject_too_short?
|
||||
add_problem(:subject_too_short, subject_description)
|
||||
end
|
||||
|
||||
if subject_too_long?
|
||||
add_problem(:subject_too_long, subject_description)
|
||||
elsif subject_above_warning?
|
||||
add_problem(:subject_above_warning, subject_description)
|
||||
end
|
||||
|
||||
if subject_starts_with_lowercase?
|
||||
add_problem(:subject_starts_with_lowercase, subject_description)
|
||||
end
|
||||
|
||||
if subject_ends_with_a_period?
|
||||
add_problem(:subject_ends_with_a_period, subject_description)
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lint_separator
|
||||
return self unless separator && !separator.empty?
|
||||
|
||||
add_problem(:separator_missing)
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def lint_details
|
||||
if !multi_line? && many_changes?
|
||||
add_problem(:details_too_many_changes)
|
||||
end
|
||||
|
||||
details&.each_line do |line|
|
||||
line = line.strip
|
||||
|
||||
next unless line_too_long?(line)
|
||||
|
||||
url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length } # rubocop:disable CodeReuse/ActiveRecord
|
||||
|
||||
# If the line includes a URL, we'll allow it to exceed MAX_LINE_LENGTH characters, but
|
||||
# only if the line _without_ the URL does not exceed this limit.
|
||||
next unless line_too_long?(line.length - url_size)
|
||||
|
||||
add_problem(:details_line_too_long)
|
||||
break
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def lint_message
|
||||
if message_contains_text_emoji?
|
||||
add_problem(:message_contains_text_emoji)
|
||||
end
|
||||
|
||||
if message_contains_unicode_emoji?
|
||||
add_problem(:message_contains_unicode_emoji)
|
||||
end
|
||||
|
||||
if message_contains_short_reference?
|
||||
add_problem(:message_contains_short_reference)
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def files_changed
|
||||
commit.diff_parent.stats[:total][:files]
|
||||
end
|
||||
|
||||
def lines_changed
|
||||
commit.diff_parent.stats[:total][:lines]
|
||||
end
|
||||
|
||||
def many_changes?
|
||||
files_changed > MAX_CHANGED_FILES_IN_COMMIT && lines_changed > MAX_CHANGED_LINES_IN_COMMIT
|
||||
end
|
||||
|
||||
def subject
|
||||
message_parts[0]
|
||||
end
|
||||
|
||||
def separator
|
||||
message_parts[1]
|
||||
end
|
||||
|
||||
def details
|
||||
message_parts[2]
|
||||
end
|
||||
|
||||
def line_too_long?(line)
|
||||
case line
|
||||
when String
|
||||
line.length > MAX_LINE_LENGTH
|
||||
when Integer
|
||||
line > MAX_LINE_LENGTH
|
||||
else
|
||||
raise ArgumentError, "The line argument (#{line}) should be a String or an Integer! #{line.class} given."
|
||||
end
|
||||
end
|
||||
|
||||
def subject_too_short?
|
||||
subject.split(' ').length < MIN_SUBJECT_WORDS_COUNT
|
||||
end
|
||||
|
||||
def subject_too_long?
|
||||
line_too_long?(subject)
|
||||
end
|
||||
|
||||
def subject_above_warning?
|
||||
subject.length > WARN_SUBJECT_LENGTH
|
||||
end
|
||||
|
||||
def subject_starts_with_lowercase?
|
||||
first_char = subject[0]
|
||||
|
||||
first_char.downcase == first_char
|
||||
end
|
||||
|
||||
def subject_ends_with_a_period?
|
||||
subject.end_with?('.')
|
||||
end
|
||||
|
||||
def message_contains_text_emoji?
|
||||
emoji_checker.includes_text_emoji?(commit.message)
|
||||
end
|
||||
|
||||
def message_contains_unicode_emoji?
|
||||
emoji_checker.includes_unicode_emoji?(commit.message)
|
||||
end
|
||||
|
||||
def message_contains_short_reference?
|
||||
commit.message.match?(SHORT_REFERENCE_REGEX)
|
||||
end
|
||||
|
||||
def emoji_checker
|
||||
@emoji_checker ||= Gitlab::Danger::EmojiChecker.new
|
||||
end
|
||||
|
||||
def message_parts
|
||||
@message_parts ||= commit.message.split("\n", 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
45
lib/gitlab/danger/emoji_checker.rb
Normal file
45
lib/gitlab/danger/emoji_checker.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
|
||||
module Gitlab
|
||||
module Danger
|
||||
class EmojiChecker
|
||||
DIGESTS = File.expand_path('../../../fixtures/emojis/digests.json', __dir__)
|
||||
ALIASES = File.expand_path('../../../fixtures/emojis/aliases.json', __dir__)
|
||||
|
||||
# A regex that indicates a piece of text _might_ include an Emoji. The regex
|
||||
# alone is not enough, as we'd match `:foo:bar:baz`. Instead, we use this
|
||||
# regex to save us from having to check for all possible emoji names when we
|
||||
# know one definitely is not included.
|
||||
LIKELY_EMOJI = /:[\+a-z0-9_\-]+:/.freeze
|
||||
|
||||
UNICODE_EMOJI_REGEX = %r{(
|
||||
[\u{1F300}-\u{1F5FF}] |
|
||||
[\u{1F1E6}-\u{1F1FF}] |
|
||||
[\u{2700}-\u{27BF}] |
|
||||
[\u{1F900}-\u{1F9FF}] |
|
||||
[\u{1F600}-\u{1F64F}] |
|
||||
[\u{1F680}-\u{1F6FF}] |
|
||||
[\u{2600}-\u{26FF}]
|
||||
)}x.freeze
|
||||
|
||||
def initialize
|
||||
names = JSON.parse(File.read(DIGESTS)).keys +
|
||||
JSON.parse(File.read(ALIASES)).keys
|
||||
|
||||
@emoji = names.map { |name| ":#{name}:" }
|
||||
end
|
||||
|
||||
def includes_text_emoji?(text)
|
||||
return false unless text.match?(LIKELY_EMOJI)
|
||||
|
||||
@emoji.any? { |emoji| text.include?(emoji) }
|
||||
end
|
||||
|
||||
def includes_unicode_emoji?(text)
|
||||
text.match?(UNICODE_EMOJI_REGEX)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -174,6 +174,10 @@ module Gitlab
|
|||
labels - current_mr_labels
|
||||
end
|
||||
|
||||
def sanitize_mr_title(title)
|
||||
title.gsub(/^WIP: */, '').gsub(/`/, '\\\`')
|
||||
end
|
||||
|
||||
def security_mr?
|
||||
return false unless gitlab_helper
|
||||
|
||||
|
|
15
lib/gitlab/error_tracking/repo.rb
Normal file
15
lib/gitlab/error_tracking/repo.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ErrorTracking
|
||||
class Repo
|
||||
attr_accessor :status, :integration_id, :project_id
|
||||
|
||||
def initialize(status:, integration_id:, project_id:)
|
||||
@status = status
|
||||
@integration_id = integration_id
|
||||
@project_id = project_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,6 +3,8 @@
|
|||
module Gitlab
|
||||
module ImportExport
|
||||
class RelationFactory
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
prepend_if_ee('::EE::Gitlab::ImportExport::RelationFactory') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
|
||||
OVERRIDES = { snippets: :project_snippets,
|
||||
|
@ -40,7 +42,7 @@ module Gitlab
|
|||
|
||||
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
|
||||
|
||||
EXISTING_OBJECT_CHECK = %i[
|
||||
EXISTING_OBJECT_RELATIONS = %i[
|
||||
milestone
|
||||
milestones
|
||||
label
|
||||
|
@ -58,9 +60,6 @@ module Gitlab
|
|||
|
||||
TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
|
||||
|
||||
# This represents all relations that have unique key on `project_id`
|
||||
UNIQUE_RELATIONS = %i[project_feature ProjectCiCdSetting container_expiration_policy].freeze
|
||||
|
||||
def self.create(*args)
|
||||
new(*args).create
|
||||
end
|
||||
|
@ -115,12 +114,18 @@ module Gitlab
|
|||
OVERRIDES
|
||||
end
|
||||
|
||||
def self.existing_object_check
|
||||
EXISTING_OBJECT_CHECK
|
||||
def self.existing_object_relations
|
||||
EXISTING_OBJECT_RELATIONS
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def existing_object?
|
||||
strong_memoize(:_existing_object) do
|
||||
self.class.existing_object_relations.include?(@relation_name) || unique_relation?
|
||||
end
|
||||
end
|
||||
|
||||
def setup_models
|
||||
case @relation_name
|
||||
when :merge_request_diff_files then setup_diff
|
||||
|
@ -229,7 +234,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def update_group_references
|
||||
return unless self.class.existing_object_check.include?(@relation_name)
|
||||
return unless existing_object?
|
||||
return unless @relation_hash['group_id']
|
||||
|
||||
@relation_hash['group_id'] = @project.namespace_id
|
||||
|
@ -322,7 +327,7 @@ module Gitlab
|
|||
# Only find existing records to avoid mapping tables such as milestones
|
||||
# Otherwise always create the record, skipping the extra SELECT clause.
|
||||
@existing_or_new_object ||= begin
|
||||
if self.class.existing_object_check.include?(@relation_name)
|
||||
if existing_object?
|
||||
attribute_hash = attribute_hash_for(['events'])
|
||||
|
||||
existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
|
||||
|
@ -356,8 +361,43 @@ module Gitlab
|
|||
!Object.const_defined?(parsed_relation_hash['type'])
|
||||
end
|
||||
|
||||
def unique_relation?
|
||||
strong_memoize(:unique_relation) do
|
||||
project_foreign_key.present? &&
|
||||
(has_unique_index_on_project_fk? || uses_project_fk_as_primary_key?)
|
||||
end
|
||||
end
|
||||
|
||||
def has_unique_index_on_project_fk?
|
||||
cache = cached_has_unique_index_on_project_fk
|
||||
table_name = relation_class.table_name
|
||||
return cache[table_name] if cache.has_key?(table_name)
|
||||
|
||||
index_exists =
|
||||
ActiveRecord::Base.connection.index_exists?(
|
||||
relation_class.table_name,
|
||||
project_foreign_key,
|
||||
unique: true)
|
||||
|
||||
cache[table_name] = index_exists
|
||||
end
|
||||
|
||||
# Avoid unnecessary DB requests
|
||||
def cached_has_unique_index_on_project_fk
|
||||
Thread.current[:cached_has_unique_index_on_project_fk] ||= {}
|
||||
end
|
||||
|
||||
def uses_project_fk_as_primary_key?
|
||||
relation_class.primary_key == project_foreign_key
|
||||
end
|
||||
|
||||
# Should be `:project_id` for most of the cases, but this is more general
|
||||
def project_foreign_key
|
||||
relation_class.reflect_on_association(:project)&.foreign_key
|
||||
end
|
||||
|
||||
def find_or_create_object!
|
||||
if UNIQUE_RELATIONS.include?(@relation_name)
|
||||
if unique_relation?
|
||||
unique_relation_object = relation_class.find_or_create_by(project_id: @project.id)
|
||||
unique_relation_object.assign_attributes(parsed_relation_hash)
|
||||
|
||||
|
|
|
@ -22,11 +22,8 @@ module Gitlab
|
|||
def pool_size
|
||||
# heuristic constant 5 should be a config setting somewhere -- related to CPU count?
|
||||
size = 5
|
||||
if Gitlab::Runtime.sidekiq?
|
||||
# the pool will be used in a multi-threaded context
|
||||
size += Sidekiq.options[:concurrency]
|
||||
elsif Gitlab::Runtime.puma?
|
||||
size += Puma.cli_config.options[:max_threads]
|
||||
if Gitlab::Runtime.multi_threaded?
|
||||
size += Gitlab::Runtime.max_threads
|
||||
end
|
||||
|
||||
size
|
||||
|
|
|
@ -10,7 +10,7 @@ module Gitlab
|
|||
def self.server_configurator(metrics: true, arguments_logger: true, memory_killer: true, request_store: true)
|
||||
lambda do |chain|
|
||||
chain.add Gitlab::SidekiqMiddleware::Monitor
|
||||
chain.add Gitlab::SidekiqMiddleware::Metrics if metrics
|
||||
chain.add Gitlab::SidekiqMiddleware::ServerMetrics if metrics
|
||||
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if arguments_logger
|
||||
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if memory_killer
|
||||
chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware if request_store
|
||||
|
@ -27,6 +27,7 @@ module Gitlab
|
|||
def self.client_configurator
|
||||
lambda do |chain|
|
||||
chain.add Gitlab::SidekiqStatus::ClientMiddleware
|
||||
chain.add Gitlab::SidekiqMiddleware::ClientMetrics
|
||||
chain.add Labkit::Middleware::Sidekiq::Client
|
||||
end
|
||||
end
|
||||
|
|
29
lib/gitlab/sidekiq_middleware/client_metrics.rb
Normal file
29
lib/gitlab/sidekiq_middleware/client_metrics.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
class ClientMetrics < SidekiqMiddleware::Metrics
|
||||
ENQUEUED = :sidekiq_enqueued_jobs_total
|
||||
|
||||
def initialize
|
||||
@metrics = init_metrics
|
||||
end
|
||||
|
||||
def call(worker, _job, queue, _redis_pool)
|
||||
labels = create_labels(worker.class, queue)
|
||||
|
||||
@metrics.fetch(ENQUEUED).increment(labels, 1)
|
||||
|
||||
yield
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_metrics
|
||||
{
|
||||
ENQUEUED => ::Gitlab::Metrics.counter(ENQUEUED, 'Sidekiq jobs enqueued')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,68 +3,11 @@
|
|||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
class Metrics
|
||||
# SIDEKIQ_LATENCY_BUCKETS are latency histogram buckets better suited to Sidekiq
|
||||
# timeframes than the DEFAULT_BUCKET definition. Defined in seconds.
|
||||
SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze
|
||||
|
||||
TRUE_LABEL = "yes"
|
||||
FALSE_LABEL = "no"
|
||||
|
||||
def initialize
|
||||
@metrics = init_metrics
|
||||
|
||||
@metrics[:sidekiq_concurrency].set({}, Sidekiq.options[:concurrency].to_i)
|
||||
end
|
||||
|
||||
def call(worker, job, queue)
|
||||
labels = create_labels(worker.class, queue)
|
||||
queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
|
||||
|
||||
@metrics[:sidekiq_jobs_queue_duration_seconds].observe(labels, queue_duration) if queue_duration
|
||||
@metrics[:sidekiq_running_jobs].increment(labels, 1)
|
||||
|
||||
if job['retry_count'].present?
|
||||
@metrics[:sidekiq_jobs_retried_total].increment(labels, 1)
|
||||
end
|
||||
|
||||
job_succeeded = false
|
||||
monotonic_time_start = Gitlab::Metrics::System.monotonic_time
|
||||
job_thread_cputime_start = get_thread_cputime
|
||||
begin
|
||||
yield
|
||||
job_succeeded = true
|
||||
ensure
|
||||
monotonic_time_end = Gitlab::Metrics::System.monotonic_time
|
||||
job_thread_cputime_end = get_thread_cputime
|
||||
|
||||
monotonic_time = monotonic_time_end - monotonic_time_start
|
||||
job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
|
||||
|
||||
# sidekiq_running_jobs, sidekiq_jobs_failed_total should not include the job_status label
|
||||
@metrics[:sidekiq_running_jobs].increment(labels, -1)
|
||||
@metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
|
||||
|
||||
# job_status: done, fail match the job_status attribute in structured logging
|
||||
labels[:job_status] = job_succeeded ? "done" : "fail"
|
||||
@metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
|
||||
@metrics[:sidekiq_jobs_completion_seconds].observe(labels, monotonic_time)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_metrics
|
||||
{
|
||||
sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
|
||||
sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
|
||||
sidekiq_jobs_queue_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_LATENCY_BUCKETS),
|
||||
sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
|
||||
sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
|
||||
sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :all),
|
||||
sidekiq_concurrency: ::Gitlab::Metrics.gauge(:sidekiq_concurrency, 'Maximum number of Sidekiq jobs', {}, :all)
|
||||
}
|
||||
end
|
||||
|
||||
def create_labels(worker_class, queue)
|
||||
labels = { queue: queue.to_s, latency_sensitive: FALSE_LABEL, external_dependencies: FALSE_LABEL, feature_category: "", boundary: "" }
|
||||
return labels unless worker_class.include? WorkerAttributes
|
||||
|
@ -84,10 +27,6 @@ module Gitlab
|
|||
def bool_as_label(value)
|
||||
value ? TRUE_LABEL : FALSE_LABEL
|
||||
end
|
||||
|
||||
def get_thread_cputime
|
||||
defined?(Process::CLOCK_THREAD_CPUTIME_ID) ? Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) : 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
70
lib/gitlab/sidekiq_middleware/server_metrics.rb
Normal file
70
lib/gitlab/sidekiq_middleware/server_metrics.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
class ServerMetrics < SidekiqMiddleware::Metrics
|
||||
# SIDEKIQ_LATENCY_BUCKETS are latency histogram buckets better suited to Sidekiq
|
||||
# timeframes than the DEFAULT_BUCKET definition. Defined in seconds.
|
||||
SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze
|
||||
|
||||
def initialize
|
||||
@metrics = init_metrics
|
||||
|
||||
@metrics[:sidekiq_concurrency].set({}, Sidekiq.options[:concurrency].to_i)
|
||||
end
|
||||
|
||||
def call(worker, job, queue)
|
||||
labels = create_labels(worker.class, queue)
|
||||
queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
|
||||
|
||||
@metrics[:sidekiq_jobs_queue_duration_seconds].observe(labels, queue_duration) if queue_duration
|
||||
@metrics[:sidekiq_running_jobs].increment(labels, 1)
|
||||
|
||||
if job['retry_count'].present?
|
||||
@metrics[:sidekiq_jobs_retried_total].increment(labels, 1)
|
||||
end
|
||||
|
||||
job_succeeded = false
|
||||
monotonic_time_start = Gitlab::Metrics::System.monotonic_time
|
||||
job_thread_cputime_start = get_thread_cputime
|
||||
begin
|
||||
yield
|
||||
job_succeeded = true
|
||||
ensure
|
||||
monotonic_time_end = Gitlab::Metrics::System.monotonic_time
|
||||
job_thread_cputime_end = get_thread_cputime
|
||||
|
||||
monotonic_time = monotonic_time_end - monotonic_time_start
|
||||
job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
|
||||
|
||||
# sidekiq_running_jobs, sidekiq_jobs_failed_total should not include the job_status label
|
||||
@metrics[:sidekiq_running_jobs].increment(labels, -1)
|
||||
@metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
|
||||
|
||||
# job_status: done, fail match the job_status attribute in structured logging
|
||||
labels[:job_status] = job_succeeded ? "done" : "fail"
|
||||
@metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
|
||||
@metrics[:sidekiq_jobs_completion_seconds].observe(labels, monotonic_time)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_metrics
|
||||
{
|
||||
sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
|
||||
sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
|
||||
sidekiq_jobs_queue_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_LATENCY_BUCKETS),
|
||||
sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
|
||||
sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
|
||||
sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :all),
|
||||
sidekiq_concurrency: ::Gitlab::Metrics.gauge(:sidekiq_concurrency, 'Maximum number of Sidekiq jobs', {}, :all)
|
||||
}
|
||||
end
|
||||
|
||||
def get_thread_cputime
|
||||
defined?(Process::CLOCK_THREAD_CPUTIME_ID) ? Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) : 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,8 @@ module Sentry
|
|||
include Sentry::Client::Event
|
||||
include Sentry::Client::Projects
|
||||
include Sentry::Client::Issue
|
||||
include Sentry::Client::Repo
|
||||
include Sentry::Client::IssueLink
|
||||
|
||||
Error = Class.new(StandardError)
|
||||
MissingKeysError = Class.new(StandardError)
|
||||
|
@ -79,7 +81,7 @@ module Sentry
|
|||
end
|
||||
|
||||
def handle_response(response)
|
||||
unless response.code == 200
|
||||
unless response.code.between?(200, 204)
|
||||
raise_error "Sentry response status code: #{response.code}"
|
||||
end
|
||||
|
||||
|
|
27
lib/sentry/client/issue_link.rb
Normal file
27
lib/sentry/client/issue_link.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sentry
|
||||
class Client
|
||||
module IssueLink
|
||||
def create_issue_link(integration_id, sentry_issue_identifier, issue)
|
||||
issue_link_url = issue_link_api_url(integration_id, sentry_issue_identifier)
|
||||
|
||||
params = {
|
||||
project: issue.project.id,
|
||||
externalIssue: "#{issue.project.id}##{issue.iid}"
|
||||
}
|
||||
|
||||
http_put(issue_link_url, params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def issue_link_api_url(integration_id, sentry_issue_identifier)
|
||||
issue_link_url = URI(url)
|
||||
issue_link_url.path = "/api/0/groups/#{sentry_issue_identifier}/integrations/#{integration_id}/"
|
||||
|
||||
issue_link_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
38
lib/sentry/client/repo.rb
Normal file
38
lib/sentry/client/repo.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sentry
|
||||
class Client
|
||||
module Repo
|
||||
def repos(organization_slug)
|
||||
repos_url = repos_api_url(organization_slug)
|
||||
|
||||
repos = http_get(repos_url)[:body]
|
||||
|
||||
handle_mapping_exceptions do
|
||||
map_to_repos(repos)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def repos_api_url(organization_slug)
|
||||
repos_url = URI(url)
|
||||
repos_url.path = "/api/0/organizations/#{organization_slug}/repos/"
|
||||
|
||||
repos_url
|
||||
end
|
||||
|
||||
def map_to_repos(repos)
|
||||
repos.map(&method(:map_to_repo))
|
||||
end
|
||||
|
||||
def map_to_repo(repo)
|
||||
Gitlab::ErrorTracking::Repo.new(
|
||||
status: repo.fetch('status'),
|
||||
integration_id: repo.fetch('integrationId'),
|
||||
project_id: repo.fetch('externalSlug')
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -691,18 +691,9 @@ msgstr ""
|
|||
msgid "<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "<strong>%{created_count}</strong> created, <strong>%{accepted_count}</strong> accepted."
|
||||
msgstr ""
|
||||
|
||||
msgid "<strong>%{created_count}</strong> created, <strong>%{closed_count}</strong> closed."
|
||||
msgstr ""
|
||||
|
||||
msgid "<strong>%{group_name}</strong> group members"
|
||||
msgstr ""
|
||||
|
||||
msgid "<strong>%{pushes}</strong> pushes, more than <strong>%{commits}</strong> commits by <strong>%{people}</strong> contributors."
|
||||
msgstr ""
|
||||
|
||||
msgid "<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes"
|
||||
msgstr ""
|
||||
|
||||
|
@ -742,6 +733,9 @@ msgstr ""
|
|||
msgid "A deleted user"
|
||||
msgstr ""
|
||||
|
||||
msgid "A file with '%{file_name}' already exists in %{branch} branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project."
|
||||
msgstr ""
|
||||
|
||||
|
@ -2423,6 +2417,9 @@ msgstr ""
|
|||
msgid "AutoDevOps|Auto DevOps"
|
||||
msgstr ""
|
||||
|
||||
msgid "AutoDevOps|Auto DevOps can automatically build, test, and deploy applications based on predefined continuous integration and delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end} or use our %{quickstart_start}quick start guide%{quickstart_end} to get started right away."
|
||||
msgstr ""
|
||||
|
||||
msgid "AutoDevOps|Auto DevOps documentation"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2999,9 +2996,6 @@ msgstr ""
|
|||
msgid "CICD|Auto DevOps"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
|
||||
msgstr ""
|
||||
|
||||
msgid "CICD|Automatic deployment to staging, manual deployment to production"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3023,9 +3017,6 @@ msgstr ""
|
|||
msgid "CICD|Jobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICD|Learn more about Auto DevOps"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found."
|
||||
msgstr ""
|
||||
|
||||
|
@ -4672,6 +4663,9 @@ msgstr ""
|
|||
msgid "Commit message"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commit message (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5046,6 +5040,42 @@ msgstr ""
|
|||
msgid "Contribution Charts"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|<strong>%{created_count}</strong> created, <strong>%{accepted_count}</strong> accepted."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|<strong>%{created_count}</strong> created, <strong>%{closed_count}</strong> closed."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|<strong>%{pushes}</strong> pushes, more than <strong>%{commits}</strong> commits by <strong>%{people}</strong> contributors."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|Contribution analytics for issues, merge requests and push events since %{start_date}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|Issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|Last 3 months"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|Last month"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|Last week"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|Merge Requests"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|No issues for the selected time period."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|No merge requests for the selected time period."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|No pushes for the selected time period."
|
||||
msgstr ""
|
||||
|
||||
msgid "Contributions for <strong>%{calendar_date}</strong>"
|
||||
msgstr ""
|
||||
|
||||
|
@ -6930,9 +6960,6 @@ msgstr ""
|
|||
msgid "Enter zen mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "EnviornmentDashboard|You are looking at the last updated environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environment"
|
||||
msgstr ""
|
||||
|
||||
|
@ -6951,6 +6978,9 @@ msgstr ""
|
|||
msgid "EnvironmentDashboard|Created through the Deployment API"
|
||||
msgstr ""
|
||||
|
||||
msgid "EnvironmentDashboard|You are looking at the last updated environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8013,6 +8043,9 @@ msgstr ""
|
|||
msgid "File moved"
|
||||
msgstr ""
|
||||
|
||||
msgid "File name"
|
||||
msgstr ""
|
||||
|
||||
msgid "File templates"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9345,9 +9378,6 @@ msgstr ""
|
|||
msgid "GroupSettings|Auto DevOps pipeline was updated for the group"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSettings|Badges"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11605,6 +11635,9 @@ msgstr ""
|
|||
msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Create custom dashboard %{fileName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Create metric"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11614,6 +11647,15 @@ msgstr ""
|
|||
msgid "Metrics|Delete metric?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Duplicate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Duplicate dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Duplicating..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Edit metric"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11650,6 +11692,12 @@ msgstr ""
|
|||
msgid "Metrics|Show last"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|There was an error creating the dashboard."
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|There was an error creating the dashboard. %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|There was an error fetching the environments data, please try again"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11689,6 +11737,9 @@ msgstr ""
|
|||
msgid "Metrics|Y-axis label"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|You can save a copy of this dashboard to your repository so it can be customized. Select a file name and branch to save it."
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|You're about to permanently delete this metric. This cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
|
@ -12227,9 +12278,6 @@ msgstr ""
|
|||
msgid "No forks are available to you."
|
||||
msgstr ""
|
||||
|
||||
msgid "No issues for the selected time period."
|
||||
msgstr ""
|
||||
|
||||
msgid "No job log"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12248,9 +12296,6 @@ msgstr ""
|
|||
msgid "No matching results"
|
||||
msgstr ""
|
||||
|
||||
msgid "No merge requests for the selected time period."
|
||||
msgstr ""
|
||||
|
||||
msgid "No merge requests found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12278,9 +12323,6 @@ msgstr ""
|
|||
msgid "No public groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "No pushes for the selected time period."
|
||||
msgstr ""
|
||||
|
||||
msgid "No repository"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15542,6 +15584,9 @@ msgstr ""
|
|||
msgid "Request Access"
|
||||
msgstr ""
|
||||
|
||||
msgid "Request parameter %{param} is missing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Request to link SAML account must be authorized"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15728,6 +15773,12 @@ msgstr ""
|
|||
msgid "Review"
|
||||
msgstr ""
|
||||
|
||||
msgid "Review App|View app"
|
||||
msgstr ""
|
||||
|
||||
msgid "Review App|View latest app"
|
||||
msgstr ""
|
||||
|
||||
msgid "Review the process for configuring service providers in your identity provider — in this case, GitLab is the \"service provider\" or \"relying party\"."
|
||||
msgstr ""
|
||||
|
||||
|
@ -18283,6 +18334,9 @@ msgstr ""
|
|||
msgid "The file has been successfully deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "The file name should have a .yml extension"
|
||||
msgstr ""
|
||||
|
||||
msgid "The following items will NOT be exported:"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18594,6 +18648,12 @@ msgstr ""
|
|||
msgid "There was an error adding a To Do."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error creating the dashboard, branch name is invalid."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error creating the dashboard, branch named: %{branch} already exists."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error creating the issue"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20485,9 +20545,6 @@ msgstr ""
|
|||
msgid "View Documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "View app"
|
||||
msgstr ""
|
||||
|
||||
msgid "View blame prior to this change"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20547,9 +20604,6 @@ msgstr ""
|
|||
msgid "View open merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "View previous app"
|
||||
msgstr ""
|
||||
|
||||
msgid "View project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20562,6 +20616,9 @@ msgstr ""
|
|||
msgid "View the documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "View the latest successful deployment to this environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Viewing commit"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21164,6 +21221,9 @@ msgstr ""
|
|||
msgid "You can try again using %{begin_link}basic search%{end_link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "You can't commit to this project"
|
||||
msgstr ""
|
||||
|
||||
msgid "You cannot access the raw file. Please wait a minute."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21491,6 +21551,9 @@ msgstr ""
|
|||
msgid "Your comment could not be updated! Please check your network connection and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your deployment services will be broken, you will need to manually fix the services after renaming."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -72,4 +72,5 @@ Disallow: /*/*/protected_branches
|
|||
Disallow: /*/*/uploads/
|
||||
Disallow: /*/-/group_members
|
||||
Disallow: /*/project_members
|
||||
Disallow: /groups/*/-/contribution_analytics
|
||||
Disallow: /groups/*/-/analytics
|
||||
|
|
|
@ -37,142 +37,70 @@ describe Projects::PerformanceMonitoring::DashboardsController do
|
|||
end
|
||||
|
||||
context 'valid parameters' do
|
||||
it 'delegates commit creation to service' do
|
||||
it 'delegates cloning to ::Metrics::Dashboard::CloneDashboardService' do
|
||||
allow(controller).to receive(:repository).and_return(repository)
|
||||
allow(repository).to receive(:find_branch).and_return(branch)
|
||||
dashboard_attrs = {
|
||||
dashboard: dashboard,
|
||||
file_name: file_name,
|
||||
commit_message: commit_message,
|
||||
branch_name: branch_name,
|
||||
start_branch: 'master',
|
||||
encoding: 'text',
|
||||
file_path: '.gitlab/dashboards/custom_dashboard.yml',
|
||||
file_content: File.read('config/prometheus/common_metrics.yml')
|
||||
branch: branch_name
|
||||
}
|
||||
|
||||
service_instance = instance_double(::Files::CreateService)
|
||||
expect(::Files::CreateService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance)
|
||||
expect(service_instance).to receive(:execute).and_return(status: :success)
|
||||
service_instance = instance_double(::Metrics::Dashboard::CloneDashboardService)
|
||||
expect(::Metrics::Dashboard::CloneDashboardService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance)
|
||||
expect(service_instance).to receive(:execute).and_return(status: :success, http_status: :created, dashboard: { path: 'dashboard/path' })
|
||||
|
||||
post :create, params: params
|
||||
end
|
||||
|
||||
it 'extends dashboard template path to absolute url' do
|
||||
allow(::Files::CreateService).to receive(:new).and_return(double(execute: { status: :success }))
|
||||
allow(controller).to receive(:repository).and_return(repository)
|
||||
allow(repository).to receive(:find_branch).and_return(branch)
|
||||
|
||||
expect(File).to receive(:read).with(Rails.root.join('config/prometheus/common_metrics.yml')).and_return('')
|
||||
|
||||
post :create, params: params
|
||||
end
|
||||
|
||||
context 'selected branch already exists' do
|
||||
it 'responds with :created status code', :aggregate_failures do
|
||||
repository.add_branch(user, branch_name, 'master')
|
||||
|
||||
post :create, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status :created
|
||||
end
|
||||
end
|
||||
|
||||
context 'request format json' do
|
||||
it 'returns path to new file' do
|
||||
allow(::Files::CreateService).to receive(:new).and_return(double(execute: { status: :success }))
|
||||
it 'returns services response' do
|
||||
allow(::Metrics::Dashboard::CloneDashboardService).to receive(:new).and_return(double(execute: { status: :success, dashboard: { path: ".gitlab/dashboards/#{file_name}" }, http_status: :created }))
|
||||
allow(controller).to receive(:repository).and_return(repository)
|
||||
|
||||
expect(repository).to receive(:find_branch).with(branch_name).and_return(branch)
|
||||
allow(repository).to receive(:find_branch).and_return(branch)
|
||||
|
||||
post :create, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status :created
|
||||
expect(json_response).to eq('redirect_to' => "/-/ide/project/#{namespace.path}/#{project.name}/edit/#{branch_name}/-/.gitlab/dashboards/#{file_name}")
|
||||
expect(response).to set_flash[:notice].to eq("Your dashboard has been copied. You can <a href=\"/-/ide/project/#{namespace.path}/#{project.name}/edit/#{branch_name}/-/.gitlab/dashboards/#{file_name}\">edit it here</a>.")
|
||||
expect(json_response).to eq('status' => 'success', 'dashboard' => { 'path' => ".gitlab/dashboards/#{file_name}" })
|
||||
end
|
||||
|
||||
context 'files create service failure' do
|
||||
it 'returns json with failure message' do
|
||||
allow(::Files::CreateService).to receive(:new).and_return(double(execute: { status: false, message: 'something went wrong' }))
|
||||
context 'Metrics::Dashboard::CloneDashboardService failure' do
|
||||
it 'returns json with failure message', :aggregate_failures do
|
||||
allow(::Metrics::Dashboard::CloneDashboardService).to receive(:new).and_return(double(execute: { status: :error, message: 'something went wrong', http_status: :bad_request }))
|
||||
|
||||
post :create, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status :bad_request
|
||||
expect(response).to set_flash[:alert].to eq('something went wrong')
|
||||
expect(json_response).to eq('error' => 'something went wrong')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'request format html' do
|
||||
before do
|
||||
params.delete(:format)
|
||||
end
|
||||
%w(commit_message file_name dashboard).each do |param|
|
||||
context "param #{param} is missing" do
|
||||
let(param.to_s) { nil }
|
||||
|
||||
it 'redirects to ide with new file' do
|
||||
allow(::Files::CreateService).to receive(:new).and_return(double(execute: { status: :success }))
|
||||
allow(controller).to receive(:repository).and_return(repository)
|
||||
it 'responds with bad request status and error message', :aggregate_failures do
|
||||
post :create, params: params
|
||||
|
||||
expect(repository).to receive(:find_branch).with(branch_name).and_return(branch)
|
||||
|
||||
post :create, params: params
|
||||
|
||||
expect(response).to redirect_to "/-/ide/project/#{namespace.path}/#{project.name}/edit/#{branch_name}/-/.gitlab/dashboards/#{file_name}"
|
||||
end
|
||||
|
||||
context 'files create service failure' do
|
||||
it 'redirects back and sets alert' do
|
||||
allow(::Files::CreateService).to receive(:new).and_return(double(execute: { status: false, message: 'something went wrong' }))
|
||||
allow(controller).to receive(:repository).and_return(repository)
|
||||
allow(repository).to receive(:find_branch).and_return(branch)
|
||||
|
||||
post :create, params: params
|
||||
|
||||
expect(response).to set_flash[:alert].to eq('something went wrong')
|
||||
expect(response).to redirect_to namespace_project_environments_path
|
||||
expect(response).to have_gitlab_http_status :bad_request
|
||||
expect(json_response).to eq('error' => "Request parameter #{param} is missing.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid dashboard template' do
|
||||
let(:dashboard) { 'config/database.yml' }
|
||||
context "param branch_name is missing" do
|
||||
let(:branch_name) { nil }
|
||||
|
||||
it 'responds 404 not found' do
|
||||
post :create, params: params
|
||||
it 'responds with bad request status and error message', :aggregate_failures do
|
||||
post :create, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status :not_found
|
||||
end
|
||||
end
|
||||
|
||||
context 'missing commit message' do
|
||||
before do
|
||||
params.delete(:commit_message)
|
||||
end
|
||||
|
||||
it 'use default commit message' do
|
||||
allow(controller).to receive(:repository).and_return(repository)
|
||||
allow(repository).to receive(:find_branch).and_return(branch)
|
||||
dashboard_attrs = {
|
||||
commit_message: 'Create custom dashboard custom_dashboard.yml',
|
||||
branch_name: branch_name,
|
||||
start_branch: 'master',
|
||||
encoding: 'text',
|
||||
file_path: ".gitlab/dashboards/custom_dashboard.yml",
|
||||
file_content: File.read('config/prometheus/common_metrics.yml')
|
||||
}
|
||||
|
||||
service_instance = instance_double(::Files::CreateService)
|
||||
expect(::Files::CreateService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance)
|
||||
expect(service_instance).to receive(:execute).and_return(status: :success)
|
||||
|
||||
post :create, params: params
|
||||
end
|
||||
end
|
||||
|
||||
context 'missing branch' do
|
||||
let(:branch_name) { nil }
|
||||
|
||||
it 'raises ActionController::ParameterMissing' do
|
||||
expect { post :create, params: params }.to raise_error ActionController::ParameterMissing
|
||||
expect(response).to have_gitlab_http_status :bad_request
|
||||
expect(json_response).to eq('error' => "Request parameter branch is missing.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
7
spec/fixtures/sentry/issue_link_sample_response.json
vendored
Normal file
7
spec/fixtures/sentry/issue_link_sample_response.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"url": "https://gitlab.com/test/tanuki-inc/issues/3",
|
||||
"integrationId": 44444,
|
||||
"displayName": "test/tanuki-inc#3",
|
||||
"id": 140319,
|
||||
"key": "gitlab.com/test:test/tanuki-inc#3"
|
||||
}
|
15
spec/fixtures/sentry/repos_sample_response.json
vendored
Normal file
15
spec/fixtures/sentry/repos_sample_response.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
[
|
||||
{
|
||||
"status": "active",
|
||||
"integrationId": "48066",
|
||||
"externalSlug": 139,
|
||||
"name": "test / tanuki-inc",
|
||||
"provider": {
|
||||
"id": "integrations:gitlab",
|
||||
"name": "Gitlab"
|
||||
},
|
||||
"url": "https://gitlab.com/test/tanuki-inc",
|
||||
"id": "52480",
|
||||
"dateCreated": "2020-01-08T21:15:17.181520Z"
|
||||
}
|
||||
]
|
|
@ -11,7 +11,6 @@ describe('Issuable suggestions app component', () => {
|
|||
search,
|
||||
projectPath: 'project',
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ describe('Issuable suggestions suggestion component', () => {
|
|||
...suggestion,
|
||||
},
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ describe('Issuable component', () => {
|
|||
baseUrl: TEST_BASE_URL,
|
||||
...props,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ describe('Issuables list component', () => {
|
|||
emptySvgPath: TEST_EMPTY_SVG_PATH,
|
||||
...props,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import statusCodes from '~/lib/utils/http_status';
|
||||
import { metricStates } from '~/monitoring/constants';
|
||||
import Dashboard from '~/monitoring/components/dashboard.vue';
|
||||
|
||||
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
|
||||
import DateTimePicker from '~/monitoring/components/date_time_picker/date_time_picker.vue';
|
||||
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
|
||||
import { createStore } from '~/monitoring/stores';
|
||||
|
@ -465,7 +467,7 @@ describe('Dashboard', () => {
|
|||
wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
const dashboardDropdown = wrapper.find('.js-dashboards-dropdown');
|
||||
const dashboardDropdown = wrapper.find(DashboardsDropdown);
|
||||
|
||||
expect(dashboardDropdown.exists()).toBe(true);
|
||||
done();
|
||||
|
|
249
spec/frontend/monitoring/components/dashboards_dropdown_spec.js
Normal file
249
spec/frontend/monitoring/components/dashboards_dropdown_spec.js
Normal file
|
@ -0,0 +1,249 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert } from '@gitlab/ui';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
|
||||
import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
|
||||
|
||||
import { dashboardGitResponse } from '../mock_data';
|
||||
|
||||
const defaultBranch = 'master';
|
||||
|
||||
function createComponent(props, opts = {}) {
|
||||
const storeOpts = {
|
||||
methods: {
|
||||
duplicateSystemDashboard: jest.fn(),
|
||||
},
|
||||
computed: {
|
||||
allDashboards: () => dashboardGitResponse,
|
||||
},
|
||||
};
|
||||
|
||||
return shallowMount(DashboardsDropdown, {
|
||||
propsData: {
|
||||
...props,
|
||||
defaultBranch,
|
||||
},
|
||||
sync: false,
|
||||
...storeOpts,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
describe('DashboardsDropdown', () => {
|
||||
let wrapper;
|
||||
|
||||
const findItems = () => wrapper.findAll(GlDropdownItem);
|
||||
const findItemAt = i => wrapper.findAll(GlDropdownItem).at(i);
|
||||
|
||||
describe('when it receives dashboards data', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
it('displays an item for each dashboard', () => {
|
||||
expect(wrapper.findAll(GlDropdownItem).length).toEqual(dashboardGitResponse.length);
|
||||
});
|
||||
|
||||
it('displays items with the dashboard display name', () => {
|
||||
expect(findItemAt(0).text()).toBe(dashboardGitResponse[0].display_name);
|
||||
expect(findItemAt(1).text()).toBe(dashboardGitResponse[1].display_name);
|
||||
expect(findItemAt(2).text()).toBe(dashboardGitResponse[2].display_name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a system dashboard is selected', () => {
|
||||
let duplicateDashboardAction;
|
||||
let modalDirective;
|
||||
|
||||
beforeEach(() => {
|
||||
modalDirective = jest.fn();
|
||||
duplicateDashboardAction = jest.fn().mockResolvedValue();
|
||||
|
||||
wrapper = createComponent(
|
||||
{
|
||||
selectedDashboard: dashboardGitResponse[0],
|
||||
},
|
||||
{
|
||||
directives: {
|
||||
GlModal: modalDirective,
|
||||
},
|
||||
methods: {
|
||||
// Mock vuex actions
|
||||
duplicateSystemDashboard: duplicateDashboardAction,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn();
|
||||
});
|
||||
|
||||
it('displays an item for each dashboard plus a "duplicate dashboard" item', () => {
|
||||
const item = wrapper.findAll({ ref: 'duplicateDashboardItem' });
|
||||
|
||||
expect(findItems().length).toEqual(dashboardGitResponse.length + 1);
|
||||
expect(item.length).toBe(1);
|
||||
});
|
||||
|
||||
describe('modal form', () => {
|
||||
let okEvent;
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
|
||||
beforeEach(() => {
|
||||
okEvent = {
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('exists and contains a form to duplicate a dashboard', () => {
|
||||
expect(findModal().exists()).toBe(true);
|
||||
expect(findModal().contains(DuplicateDashboardForm)).toBe(true);
|
||||
});
|
||||
|
||||
it('saves a new dashboard', done => {
|
||||
findModal().vm.$emit('ok', okEvent);
|
||||
|
||||
waitForPromises()
|
||||
.then(() => {
|
||||
expect(okEvent.preventDefault).toHaveBeenCalled();
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
|
||||
expect(wrapper.emitted().selectDashboard).toBeTruthy();
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
describe('when a new dashboard is saved succesfully', () => {
|
||||
const newDashboard = {
|
||||
can_edit: true,
|
||||
default: false,
|
||||
display_name: 'A new dashboard',
|
||||
system_dashboard: false,
|
||||
};
|
||||
|
||||
const submitForm = formVals => {
|
||||
duplicateDashboardAction.mockResolvedValueOnce(newDashboard);
|
||||
findModal()
|
||||
.find(DuplicateDashboardForm)
|
||||
.vm.$emit('change', {
|
||||
dashboard: 'common_metrics.yml',
|
||||
commitMessage: 'A commit message',
|
||||
...formVals,
|
||||
});
|
||||
findModal().vm.$emit('ok', okEvent);
|
||||
};
|
||||
|
||||
it('to the default branch, redirects to the new dashboard', done => {
|
||||
submitForm({
|
||||
branch: defaultBranch,
|
||||
});
|
||||
|
||||
waitForPromises()
|
||||
.then(() => {
|
||||
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('to a new branch refreshes in the current dashboard', done => {
|
||||
submitForm({
|
||||
branch: 'another-branch',
|
||||
});
|
||||
|
||||
waitForPromises()
|
||||
.then(() => {
|
||||
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles error when a new dashboard is not saved', done => {
|
||||
const errMsg = 'An error occurred';
|
||||
|
||||
duplicateDashboardAction.mockRejectedValueOnce(errMsg);
|
||||
findModal().vm.$emit('ok', okEvent);
|
||||
|
||||
waitForPromises()
|
||||
.then(() => {
|
||||
expect(okEvent.preventDefault).toHaveBeenCalled();
|
||||
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlert().text()).toBe(errMsg);
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('id is correct, as the value of modal directive binding matches modal id', () => {
|
||||
expect(modalDirective).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Binding's second argument contains the modal id
|
||||
expect(modalDirective.mock.calls[0][1]).toEqual(
|
||||
expect.objectContaining({
|
||||
value: findModal().props('modalId'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the form on changes', () => {
|
||||
const formVals = {
|
||||
dashboard: 'common_metrics.yml',
|
||||
commitMessage: 'A commit message',
|
||||
};
|
||||
|
||||
findModal()
|
||||
.find(DuplicateDashboardForm)
|
||||
.vm.$emit('change', formVals);
|
||||
|
||||
// Binding's second argument contains the modal id
|
||||
expect(wrapper.vm.form).toEqual(formVals);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a custom dashboard is selected', () => {
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({
|
||||
selectedDashboard: dashboardGitResponse[1],
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an item for each dashboard', () => {
|
||||
const item = wrapper.findAll({ ref: 'duplicateDashboardItem' });
|
||||
|
||||
expect(findItems().length).toEqual(dashboardGitResponse.length);
|
||||
expect(item.length).toBe(0);
|
||||
});
|
||||
|
||||
it('modal form does not exist and contains a form to duplicate a dashboard', () => {
|
||||
expect(findModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a dashboard gets selected by the user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
findItemAt(1).vm.$emit('click');
|
||||
});
|
||||
|
||||
it('emits a "selectDashboard" event', () => {
|
||||
expect(wrapper.emitted().selectDashboard).toBeTruthy();
|
||||
});
|
||||
it('emits a "selectDashboard" event with dashboard information', () => {
|
||||
expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[1]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,153 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
|
||||
|
||||
import { dashboardGitResponse } from '../mock_data';
|
||||
|
||||
describe('DuplicateDashboardForm', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultBranch = 'master';
|
||||
|
||||
const findByRef = ref => wrapper.find({ ref });
|
||||
const setValue = (ref, val) => {
|
||||
findByRef(ref).setValue(val);
|
||||
};
|
||||
const setChecked = value => {
|
||||
const input = wrapper.find(`.form-check-input[value="${value}"]`);
|
||||
input.element.checked = true;
|
||||
input.trigger('click');
|
||||
input.trigger('change');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Use `mount` to render native input elements
|
||||
wrapper = mount(DuplicateDashboardForm, {
|
||||
propsData: {
|
||||
dashboard: dashboardGitResponse[0],
|
||||
defaultBranch,
|
||||
},
|
||||
sync: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
expect(wrapper.exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('renders form elements', () => {
|
||||
expect(findByRef('fileName').exists()).toEqual(true);
|
||||
expect(findByRef('branchName').exists()).toEqual(true);
|
||||
expect(findByRef('branchOption').exists()).toEqual(true);
|
||||
expect(findByRef('commitMessage').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
describe('validates the file name', () => {
|
||||
const findInvalidFeedback = () => findByRef('fileNameFormGroup').find('.invalid-feedback');
|
||||
|
||||
it('when is empty', done => {
|
||||
setValue('fileName', '');
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
|
||||
expect(findInvalidFeedback().exists()).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('when is valid', done => {
|
||||
setValue('fileName', 'my_dashboard.yml');
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
|
||||
expect(findInvalidFeedback().exists()).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('when is not valid', done => {
|
||||
setValue('fileName', 'my_dashboard.exe');
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(findByRef('fileNameFormGroup').is('.is-invalid')).toBe(true);
|
||||
expect(findInvalidFeedback().text()).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('emits `change` event', () => {
|
||||
const lastChange = () =>
|
||||
wrapper.vm.$nextTick().then(() => {
|
||||
wrapper.find('form').trigger('change');
|
||||
|
||||
// Resolves to the last emitted change
|
||||
const changes = wrapper.emitted().change;
|
||||
return changes[changes.length - 1][0];
|
||||
});
|
||||
|
||||
it('with the inital form values', () => {
|
||||
expect(wrapper.emitted().change).toHaveLength(1);
|
||||
expect(lastChange()).resolves.toEqual({
|
||||
branch: '',
|
||||
commitMessage: expect.any(String),
|
||||
dashboard: dashboardGitResponse[0].path,
|
||||
fileName: 'common_metrics.yml',
|
||||
});
|
||||
});
|
||||
|
||||
it('containing an inputted file name', () => {
|
||||
setValue('fileName', 'my_dashboard.yml');
|
||||
|
||||
expect(lastChange()).resolves.toMatchObject({
|
||||
fileName: 'my_dashboard.yml',
|
||||
});
|
||||
});
|
||||
|
||||
it('containing a default commit message when no message is set', () => {
|
||||
setValue('commitMessage', '');
|
||||
|
||||
expect(lastChange()).resolves.toMatchObject({
|
||||
commitMessage: expect.stringContaining('Create custom dashboard'),
|
||||
});
|
||||
});
|
||||
|
||||
it('containing an inputted commit message', () => {
|
||||
setValue('commitMessage', 'My commit message');
|
||||
|
||||
expect(lastChange()).resolves.toMatchObject({
|
||||
commitMessage: expect.stringContaining('My commit message'),
|
||||
});
|
||||
});
|
||||
|
||||
it('containing an inputted branch name', () => {
|
||||
setValue('branchName', 'a-new-branch');
|
||||
|
||||
expect(lastChange()).resolves.toMatchObject({
|
||||
branch: 'a-new-branch',
|
||||
});
|
||||
});
|
||||
|
||||
it('when a `default` branch option is set, branch input is invisible and ignored', done => {
|
||||
setChecked(wrapper.vm.$options.radioVals.DEFAULT);
|
||||
setValue('branchName', 'a-new-branch');
|
||||
|
||||
expect(lastChange()).resolves.toMatchObject({
|
||||
branch: defaultBranch,
|
||||
});
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(findByRef('branchName').isVisible()).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('when `new` branch option is chosen, focuses on the branch name input', done => {
|
||||
setChecked(wrapper.vm.$options.radioVals.NEW);
|
||||
|
||||
wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
wrapper.find('form').trigger('change');
|
||||
expect(findByRef('branchName').is(':focus')).toBe(true);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,6 +15,7 @@ export const propsData = {
|
|||
clustersPath: '/path/to/clusters',
|
||||
tagsPath: '/path/to/tags',
|
||||
projectPath: '/path/to/project',
|
||||
defaultBranch: 'master',
|
||||
metricsEndpoint: mockApiEndpoint,
|
||||
deploymentsEndpoint: null,
|
||||
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
|
||||
|
|
|
@ -522,6 +522,7 @@ export const dashboardGitResponse = [
|
|||
default: true,
|
||||
display_name: 'Default',
|
||||
can_edit: false,
|
||||
system_dashboard: true,
|
||||
project_blob_path: null,
|
||||
path: 'config/prometheus/common_metrics.yml',
|
||||
},
|
||||
|
@ -529,6 +530,7 @@ export const dashboardGitResponse = [
|
|||
default: false,
|
||||
display_name: 'Custom Dashboard 1',
|
||||
can_edit: true,
|
||||
system_dashboard: false,
|
||||
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_1.yml`,
|
||||
path: '.gitlab/dashboards/dashboard_1.yml',
|
||||
},
|
||||
|
@ -536,6 +538,7 @@ export const dashboardGitResponse = [
|
|||
default: false,
|
||||
display_name: 'Custom Dashboard 2',
|
||||
can_edit: true,
|
||||
system_dashboard: false,
|
||||
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_2.yml`,
|
||||
path: '.gitlab/dashboards/dashboard_2.yml',
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
fetchPrometheusMetric,
|
||||
setEndpoints,
|
||||
setGettingStartedEmptyState,
|
||||
duplicateSystemDashboard,
|
||||
} from '~/monitoring/stores/actions';
|
||||
import storeState from '~/monitoring/stores/state';
|
||||
import {
|
||||
|
@ -544,4 +545,85 @@ describe('Monitoring store actions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicateSystemDashboard', () => {
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = storeState();
|
||||
state.dashboardsEndpoint = '/dashboards.json';
|
||||
});
|
||||
|
||||
it('Succesful POST request resolves', done => {
|
||||
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
|
||||
dashboard: dashboardGitResponse[1],
|
||||
});
|
||||
|
||||
testAction(duplicateSystemDashboard, {}, state, [], [])
|
||||
.then(() => {
|
||||
expect(mock.history.post).toHaveLength(1);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('Succesful POST request resolves to a dashboard', done => {
|
||||
const mockCreatedDashboard = dashboardGitResponse[1];
|
||||
|
||||
const params = {
|
||||
dashboard: 'my-dashboard',
|
||||
fileName: 'file-name.yml',
|
||||
branch: 'my-new-branch',
|
||||
commitMessage: 'A new commit message',
|
||||
};
|
||||
|
||||
const expectedPayload = JSON.stringify({
|
||||
dashboard: 'my-dashboard',
|
||||
file_name: 'file-name.yml',
|
||||
branch: 'my-new-branch',
|
||||
commit_message: 'A new commit message',
|
||||
});
|
||||
|
||||
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
|
||||
dashboard: mockCreatedDashboard,
|
||||
});
|
||||
|
||||
testAction(duplicateSystemDashboard, params, state, [], [])
|
||||
.then(result => {
|
||||
expect(mock.history.post).toHaveLength(1);
|
||||
expect(mock.history.post[0].data).toEqual(expectedPayload);
|
||||
expect(result).toEqual(mockCreatedDashboard);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('Failed POST request throws an error', done => {
|
||||
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST);
|
||||
|
||||
testAction(duplicateSystemDashboard, {}, state, [], []).catch(err => {
|
||||
expect(mock.history.post).toHaveLength(1);
|
||||
expect(err).toEqual(expect.any(String));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Failed POST request throws an error with a description', done => {
|
||||
const backendErrorMsg = 'This file already exists!';
|
||||
|
||||
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST, {
|
||||
error: backendErrorMsg,
|
||||
});
|
||||
|
||||
testAction(duplicateSystemDashboard, {}, state, [], []).catch(err => {
|
||||
expect(mock.history.post).toHaveLength(1);
|
||||
expect(err).toEqual(expect.any(String));
|
||||
expect(err).toEqual(expect.stringContaining(backendErrorMsg));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -134,7 +134,7 @@ describe('Deployment component', () => {
|
|||
if (status === SUCCESS) {
|
||||
expect(wrapper.find(DeploymentViewButton).text()).toContain('View app');
|
||||
} else {
|
||||
expect(wrapper.find(DeploymentViewButton).text()).toContain('View previous app');
|
||||
expect(wrapper.find(DeploymentViewButton).text()).toContain('View latest app');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@ import DeploymentViewButton from '~/vue_merge_request_widget/components/deployme
|
|||
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
|
||||
import deploymentMockData from './deployment_mock_data';
|
||||
|
||||
const appButtonText = {
|
||||
text: 'View app',
|
||||
tooltip: 'View the latest successful deployment to this environment',
|
||||
};
|
||||
|
||||
describe('Deployment View App button', () => {
|
||||
let wrapper;
|
||||
|
||||
|
@ -16,7 +21,7 @@ describe('Deployment View App button', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
deployment: deploymentMockData,
|
||||
isCurrent: true,
|
||||
appButtonText,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -26,25 +31,8 @@ describe('Deployment View App button', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
});
|
||||
it('renders text as passed', () => {
|
||||
expect(wrapper.find(ReviewAppLink).text()).toContain(appButtonText.text);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -53,7 +41,7 @@ describe('Deployment View App button', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
deployment: { ...deploymentMockData, changes: null },
|
||||
isCurrent: false,
|
||||
appButtonText,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -68,7 +56,7 @@ describe('Deployment View App button', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] },
|
||||
isCurrent: false,
|
||||
appButtonText,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -91,7 +79,7 @@ describe('Deployment View App button', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
deployment: deploymentMockData,
|
||||
isCurrent: false,
|
||||
appButtonText,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,6 @@ describe('Changed file icon', () => {
|
|||
showTooltip: true,
|
||||
...props,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ describe('clipboard button', () => {
|
|||
const createWrapper = propsData => {
|
||||
wrapper = shallowMount(ClipboardButton, {
|
||||
propsData,
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ describe('Commit component', () => {
|
|||
const createComponent = propsData => {
|
||||
wrapper = shallowMount(CommitComponent, {
|
||||
propsData,
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ describe('IssueAssigneesComponent', () => {
|
|||
assignees: mockAssigneesList,
|
||||
...props,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
vm = wrapper.vm; // eslint-disable-line
|
||||
};
|
||||
|
|
|
@ -13,7 +13,6 @@ const createComponent = (milestone = mockMilestone) => {
|
|||
propsData: {
|
||||
milestone,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ describe('RelatedIssuableItem', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mount(RelatedIssuableItem, {
|
||||
slots,
|
||||
attachToDocument: true,
|
||||
propsData: props,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ describe('Markdown field header component', () => {
|
|||
previewMarkdown: false,
|
||||
...props,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ describe('Suggestion Diff component', () => {
|
|||
...DEFAULT_PROPS,
|
||||
...props,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ describe('modal copy button', () => {
|
|||
text: 'copy me',
|
||||
title: 'Copy this value',
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ describe('system note component', () => {
|
|||
vm = mount(IssueSystemNote, {
|
||||
store,
|
||||
propsData: props,
|
||||
attachToDocument: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ describe('Pagination links component', () => {
|
|||
list: [{ id: 'foo' }, { id: 'bar' }],
|
||||
props,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
|
||||
[glPaginatedList] = wrapper.vm.$children;
|
||||
|
|
|
@ -14,7 +14,6 @@ describe('Resizable Chart Container', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(ResizableChartContainer, {
|
||||
attachToDocument: true,
|
||||
scopedSlots: {
|
||||
default: `
|
||||
<div class="slot" slot-scope="{ width, height }">
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
const createComponent = (config = mockConfig) =>
|
||||
shallowMount(BaseComponent, {
|
||||
propsData: config,
|
||||
attachToDocument: true,
|
||||
});
|
||||
|
||||
describe('BaseComponent', () => {
|
||||
|
|
|
@ -24,7 +24,6 @@ const createComponent = (
|
|||
labelFilterBasePath,
|
||||
enableScopedLabels: true,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ describe('Time ago with tooltip component', () => {
|
|||
|
||||
const buildVm = (propsData = {}) => {
|
||||
vm = shallowMount(TimeAgoTooltip, {
|
||||
attachToDocument: true,
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -26,7 +26,6 @@ describe('User Avatar Link Component', () => {
|
|||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
attachToDocument: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -59,7 +59,6 @@ describe('User Popover Component', () => {
|
|||
status: null,
|
||||
},
|
||||
},
|
||||
attachToDocument: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
17
spec/graphql/types/environment_type_spec.rb
Normal file
17
spec/graphql/types/environment_type_spec.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe GitlabSchema.types['Environment'] do
|
||||
it { expect(described_class.graphql_name).to eq('Environment') }
|
||||
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %w[
|
||||
name id
|
||||
]
|
||||
|
||||
is_expected.to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
|
||||
it { is_expected.to require_graphql_authorizations(:read_environment) }
|
||||
end
|
|
@ -23,7 +23,7 @@ describe GitlabSchema.types['Project'] do
|
|||
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
|
||||
namespace group statistics repository merge_requests merge_request issues
|
||||
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
|
||||
grafanaIntegration autocloseReferencedIssues suggestion_commit_message
|
||||
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
|
||||
]
|
||||
|
||||
is_expected.to include_graphql_fields(*expected_fields)
|
||||
|
@ -70,4 +70,11 @@ describe GitlabSchema.types['Project'] do
|
|||
it { is_expected.to have_graphql_type(Types::GrafanaIntegrationType) }
|
||||
it { is_expected.to have_graphql_resolver(Resolvers::Projects::GrafanaIntegrationResolver) }
|
||||
end
|
||||
|
||||
describe 'environments field' do
|
||||
subject { described_class.fields['environments'] }
|
||||
|
||||
it { is_expected.to have_graphql_type(Types::EnvironmentType.connection_type) }
|
||||
it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver) }
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue