summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-16 18:08:46 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-16 18:08:46 +0000
commitaa0f0e992153e84e1cdec8a1c7310d5eb93a9f8f (patch)
tree4a662bc77fb43e1d1deec78cc7a95d911c0da1c5
parentd47f9d2304dbc3a23bba7fe7a5cd07218eeb41cd (diff)
downloadgitlab-ce-aa0f0e992153e84e1cdec8a1c7310d5eb93a9f8f.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue50
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue139
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue138
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js24
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue18
-rw-r--r--app/controllers/projects/performance_monitoring/dashboards_controller.rb77
-rw-r--r--app/graphql/resolvers/environments_resolver.rb23
-rw-r--r--app/graphql/types/environment_type.rb16
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/helpers/environments_helper.rb2
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/user.rb6
-rw-r--r--app/services/metrics/dashboard/clone_dashboard_service.rb113
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml7
-rw-r--r--changelogs/unreleased/196254-update-label-text.yml5
-rw-r--r--changelogs/unreleased/30936-ado-quick-start.yml5
-rw-r--r--changelogs/unreleased/37238-add-ability-to-duplicate-the-common-metrics-dashboard.yml5
-rw-r--r--changelogs/unreleased/environment-name-search-graphql.yml5
-rw-r--r--changelogs/unreleased/return-empty-body-for-204-responses-in-api.yml5
-rw-r--r--danger/changelog/Dangerfile8
-rw-r--r--danger/commit_messages/Dangerfile325
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql85
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json234
-rw-r--r--doc/api/graphql/reference/index.md9
-rw-r--r--doc/api/merge_request_approvals.md32
-rw-r--r--doc/development/api_styleguide.md6
-rw-r--r--doc/development/documentation/styleguide.md2
-rw-r--r--doc/development/fe_guide/frontend_faq.md68
-rw-r--r--doc/user/clusters/applications.md2
-rw-r--r--doc/user/project/integrations/prometheus.md33
-rw-r--r--doc/user/project/repository/forking_workflow.md4
-rw-r--r--lib/api/applications.rb2
-rw-r--r--lib/api/badges.rb1
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/custom_attributes_endpoints.rb2
-rw-r--r--lib/api/features.rb2
-rw-r--r--lib/api/group_milestones.rb2
-rw-r--r--lib/api/helpers.rb1
-rw-r--r--lib/api/pages.rb4
-rw-r--r--lib/api/pages_domains.rb3
-rw-r--r--lib/api/project_milestones.rb2
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/users.rb6
-rw-r--r--lib/api/variables.rb5
-rw-r--r--lib/api/wikis.rb3
-rw-r--r--lib/gitlab/danger/commit_linter.rb232
-rw-r--r--lib/gitlab/danger/emoji_checker.rb45
-rw-r--r--lib/gitlab/danger/helper.rb4
-rw-r--r--lib/gitlab/error_tracking/repo.rb15
-rw-r--r--lib/gitlab/import_export/relation_factory.rb58
-rw-r--r--lib/gitlab/redis/wrapper.rb7
-rw-r--r--lib/gitlab/sidekiq_middleware.rb3
-rw-r--r--lib/gitlab/sidekiq_middleware/client_metrics.rb29
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics.rb61
-rw-r--r--lib/gitlab/sidekiq_middleware/server_metrics.rb70
-rw-r--r--lib/sentry/client.rb4
-rw-r--r--lib/sentry/client/issue_link.rb27
-rw-r--r--lib/sentry/client/repo.rb38
-rw-r--r--locale/gitlab.pot135
-rw-r--r--public/robots.txt1
-rw-r--r--spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb130
-rw-r--r--spec/fixtures/sentry/issue_link_sample_response.json7
-rw-r--r--spec/fixtures/sentry/repos_sample_response.json15
-rw-r--r--spec/frontend/issuable_suggestions/components/app_spec.js1
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js1
-rw-r--r--spec/frontend/issuables_list/components/issuable_spec.js1
-rw-r--r--spec/frontend/issuables_list/components/issuables_list_app_spec.js1
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js249
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js153
-rw-r--r--spec/frontend/monitoring/init_utils.js1
-rw-r--r--spec/frontend/monitoring/mock_data.js3
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js82
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/paginated_list_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart_container_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/time_ago_tooltip_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js1
-rw-r--r--spec/graphql/types/environment_type_spec.rb17
-rw-r--r--spec/graphql/types/project_type_spec.rb9
-rw-r--r--spec/helpers/environments_helper_spec.rb5
-rw-r--r--spec/javascripts/monitoring/components/dashboard_resize_spec.js1
-rw-r--r--spec/javascripts/vue_mr_widget/components/review_app_link_spec.js5
-rw-r--r--spec/lib/gitlab/danger/changelog_spec.rb12
-rw-r--r--spec/lib/gitlab/danger/commit_linter_spec.rb315
-rw-r--r--spec/lib/gitlab/danger/emoji_checker_spec.rb38
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb13
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/import_test_coverage_spec.rb125
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb113
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb (renamed from spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb)4
-rw-r--r--spec/lib/gitlab/sidekiq_middleware_spec.rb4
-rw-r--r--spec/lib/sentry/client/issue_link_spec.rb41
-rw-r--r--spec/lib/sentry/client/repo_spec.rb39
-rw-r--r--spec/lib/sentry/client_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb32
-rw-r--r--spec/models/user_spec.rb23
-rw-r--r--spec/services/metrics/dashboard/clone_dashboard_service_spec.rb197
-rw-r--r--spec/support/helpers/metrics_dashboard_helpers.rb50
-rw-r--r--spec/support/redis/redis_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/lib/sentry/client_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/status_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb52
-rw-r--r--vendor/elastic_stack/values.yaml30
125 files changed, 3410 insertions, 717 deletions
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 797fd0e7e19..b03ee12aef3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -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
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
new file mode 100644
index 00000000000..6d93eee0b4f
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -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>
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
new file mode 100644
index 00000000000..e678957c1e5
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
@@ -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>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index fce89b450e4..61cd8621902 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -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 () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 0b848de9562..506a30ae619 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -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;
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
index e03b1e6d6a6..34866cdfa6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
@@ -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"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index 9965e3d5203..18d4073ecd4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -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"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
index 1550ec0f21e..c38c41f13b6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -1,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>
diff --git a/app/controllers/projects/performance_monitoring/dashboards_controller.rb b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
index c873fcd6c8a..2d872b78096 100644
--- a/app/controllers/projects/performance_monitoring/dashboards_controller.rb
+++ b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
@@ -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')
diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb
new file mode 100644
index 00000000000..868abef98eb
--- /dev/null
+++ b/app/graphql/resolvers/environments_resolver.rb
@@ -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
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
new file mode 100644
index 00000000000..ad65caa24a6
--- /dev/null
+++ b/app/graphql/types/environment_type.rb
@@ -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
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 31cde7b6d48..5ece4926951 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -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,
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 59972118ae3..993c18f9229 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -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),
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 663389050d1..3943d991c87 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 6442e74bbe3..4bba4d47b8f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
#
diff --git a/app/services/metrics/dashboard/clone_dashboard_service.rb b/app/services/metrics/dashboard/clone_dashboard_service.rb
new file mode 100644
index 00000000000..b2ec44cb814
--- /dev/null
+++ b/app/services/metrics/dashboard/clone_dashboard_service.rb
@@ -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')
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 3c6ad899d1e..ecbabab3e7f 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -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')
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 81bd15ed287..8c9b859e127 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -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
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index a027dca1b56..88bb0a97487 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -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')
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 5a6c8079543..a65afeecc17 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -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?
diff --git a/changelogs/unreleased/196254-update-label-text.yml b/changelogs/unreleased/196254-update-label-text.yml
new file mode 100644
index 00000000000..a44ab941b14
--- /dev/null
+++ b/changelogs/unreleased/196254-update-label-text.yml
@@ -0,0 +1,5 @@
+---
+title: Update button label in MR widget pipeline footer
+merge_request: 22900
+author:
+type: changed
diff --git a/changelogs/unreleased/30936-ado-quick-start.yml b/changelogs/unreleased/30936-ado-quick-start.yml
new file mode 100644
index 00000000000..24cf5c5f416
--- /dev/null
+++ b/changelogs/unreleased/30936-ado-quick-start.yml
@@ -0,0 +1,5 @@
+---
+title: Adds quickstart doc link to ADO CICD settings
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/37238-add-ability-to-duplicate-the-common-metrics-dashboard.yml b/changelogs/unreleased/37238-add-ability-to-duplicate-the-common-metrics-dashboard.yml
new file mode 100644
index 00000000000..5d175d3a1fc
--- /dev/null
+++ b/changelogs/unreleased/37238-add-ability-to-duplicate-the-common-metrics-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to duplicate the common metrics dashboard
+merge_request: 21929
+author:
+type: added
diff --git a/changelogs/unreleased/environment-name-search-graphql.yml b/changelogs/unreleased/environment-name-search-graphql.yml
new file mode 100644
index 00000000000..898cd7e91d6
--- /dev/null
+++ b/changelogs/unreleased/environment-name-search-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Get Project's environment names via GraphQL
+merge_request: 22932
+author:
+type: added
diff --git a/changelogs/unreleased/return-empty-body-for-204-responses-in-api.yml b/changelogs/unreleased/return-empty-body-for-204-responses-in-api.yml
new file mode 100644
index 00000000000..17344086b1a
--- /dev/null
+++ b/changelogs/unreleased/return-empty-body-for-204-responses-in-api.yml
@@ -0,0 +1,5 @@
+---
+title: Return empty body for 204 responses in API
+merge_request: 22086
+author:
+type: fixed
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
index 8c010accd56..1c4647121fb 100644
--- a/danger/changelog/Dangerfile
+++ b/danger/changelog/Dangerfile
@@ -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
diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile
index a7466aa6ffb..d6eb050930c 100644
--- a/danger/commit_messages/Dangerfile
+++ b/danger/commit_messages/Dangerfile
@@ -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}")
-end
-
-def lines_changed_in_commit(commit)
- commit.diff_parent.stats[:total][:lines]
+ self.warn("#{commit.sha}: #{message}")
end
-def subject_starts_with_capital?(subject)
- first_char = subject.chars.first
-
- first_char.upcase == first_char
-end
-
-def too_many_changed_lines?(commit)
- commit.diff_parent.stats[:total][:files] > 3 &&
- lines_changed_in_commit(commit) >= 30
-end
-
-def emoji_checker
- @emoji_checker ||= EmojiChecker.new
+def squash_mr?
+ gitlab_danger.ci? ? gitlab.mr_json['squash'] : false
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
+def wip_mr?
+ gitlab_danger.ci? ? gitlab.mr_json['work_in_progress'] : false
end
-def count_filtered_commits(commits)
- commits.count do |commit|
- !commit.message.start_with?('fixup!', 'squash!')
- end
-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 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 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 MR is set to squash, we ignore fixup commits
+ return linter if linter.fixup? && squash_mr?
- 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
- end
-
- failures = false
- subject, separator, details = commit.message.split("\n", 3)
-
- if subject.split.length < 3
- fail_commit(
- commit,
- 'The commit subject must contain at least three words'
- )
- failures = true
+ return linter
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'
- )
+ linter.lint
+end
- failures = true
- end
+def lint_mr_title(mr_title)
+ commit = Struct.new(:message, :sha).new(mr_title)
- 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'
- )
+ Gitlab::Danger::CommitLinter.new(commit).lint_subject("merge request title")
+end
- failures = true
- end
+def count_non_fixup_commits(commit_linters)
+ commit_linters.count { |commit_linter| !commit_linter.fixup? }
+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'
- )
+def lint_commits(commits)
+ 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?)
- failures = true
+ 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 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'
- )
+ if squash_mr?
+ multi_line_commit_linter = commit_linters.detect { |commit_linter| commit_linter.multi_line? }
- failures = true
+ 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
- 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
+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
-
- failures
end
-def lint_commits(commits)
- failed = commits.select do |commit|
- lint_commit(commit)
- end
+def fail_message(intro)
+ markdown(<<~MARKDOWN)
+ ## Commit message standards
- if failed.any?
- markdown(<<~MARKDOWN)
- ## Commit message standards
+ #{intro}
- 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}).
+ 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:
+ Here is an example of a good commit message:
- Reject ruby interpolation in externalized strings
+ 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.
+ 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.
+ To mix variables into translations we need to use `sprintf`
+ instead.
- Instead of:
+ Instead of:
- _("Hello \#{subject}")
+ _("Hello \#{subject}")
- Use:
+ Use:
- _("Hello %{subject}") % { subject: 'world' }
+ _("Hello %{subject}") % { subject: 'world' }
- This is an example of a bad commit message:
+ This is an example of a bad commit message:
- updated README.md
+ 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
+ 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
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 5d534e6a075..206590092a4 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -1413,6 +1413,56 @@ enum EntryType {
}
"""
+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.
"""
type Epic implements Noteable {
@@ -4707,6 +4757,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
"""
forksCount: Int!
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index f7ab02e9e9a..0240876c53d 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -407,6 +407,79 @@
"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",
"args": [
@@ -15433,6 +15506,167 @@
},
{
"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",
"description": null,
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index ef6d316e7d6..4a9cbed7dca 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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.
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index 264e198c596..f98fb7a5a0d 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -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:
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
index 71963ee0c0a..d5fc24c1ddb 100644
--- a/doc/development/api_styleguide.md
+++ b/doc/development/api_styleguide.md
@@ -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
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 385569fc8fa..fd591c71e85 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -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:**
diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md
index b8101a99ff6..01ed07f8736 100644
--- a/doc/development/fe_guide/frontend_faq.md
+++ b/doc/development/fe_guide/frontend_faq.md
@@ -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);
+};
+```
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 5c9b8b56cbd..507c027d43f 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -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.
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index a5c666f08a6..17e64f1692d 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -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:**
diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md
index 4cf0e458a53..a879f534511 100644
--- a/doc/user/project/repository/forking_workflow.md
+++ b/doc/user/project/repository/forking_workflow.md
@@ -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
diff --git a/lib/api/applications.rb b/lib/api/applications.rb
index 92717e04543..4e9843e17e8 100644
--- a/lib/api/applications.rb
+++ b/lib/api/applications.rb
@@ -38,7 +38,7 @@ module API
application = ApplicationsFinder.new(params).execute
application.destroy
- status 204
+ no_content!
end
end
end
diff --git a/lib/api/badges.rb b/lib/api/badges.rb
index e987c24c707..d2152fad07b 100644
--- a/lib/api/badges.rb
+++ b/lib/api/badges.rb
@@ -135,7 +135,6 @@ module API
end
destroy_conditionally!(badge)
- body false
end
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index ce3ee0d7e61..999bf1627c1 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -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])
diff --git a/lib/api/custom_attributes_endpoints.rb b/lib/api/custom_attributes_endpoints.rb
index 2149e04451e..ef1264126f4 100644
--- a/lib/api/custom_attributes_endpoints.rb
+++ b/lib/api/custom_attributes_endpoints.rb
@@ -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
diff --git a/lib/api/features.rb b/lib/api/features.rb
index 4dc1834c644..69b751e9bdb 100644
--- a/lib/api/features.rb
+++ b/lib/api/features.rb
@@ -74,7 +74,7 @@ module API
delete ':name' do
Feature.get(params[:name]).remove
- status 204
+ no_content!
end
end
end
diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb
index eae29f5b5dd..9e9f5101285 100644
--- a/lib/api/group_milestones.rb
+++ b/lib/api/group_milestones.rb
@@ -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
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 1fe2988ec1c..b2f5def4048 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -31,6 +31,7 @@ module API
check_unmodified_since!(last_updated)
status 204
+ body false
if block_given?
yield resource
diff --git a/lib/api/pages.rb b/lib/api/pages.rb
index 39c8f1e6bdf..ee7fe669519 100644
--- a/lib/api/pages.rb
+++ b/lib/api/pages.rb
@@ -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
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index 9f8c1e4f916..4c3d2d131ac 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -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
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
index aebf7d5fae1..8643854a655 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -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
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 3e61b3c7f3b..2271131ced3 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -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
diff --git a/lib/api/users.rb b/lib/api/users.rb
index b8c60f1969c..bf1fe4fc4a8 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -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
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index f022b9e665a..192b06b8a1b 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -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
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index eb36779e1d7..a2146406690 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -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
diff --git a/lib/gitlab/danger/commit_linter.rb b/lib/gitlab/danger/commit_linter.rb
new file mode 100644
index 00000000000..c0748a4b8e6
--- /dev/null
+++ b/lib/gitlab/danger/commit_linter.rb
@@ -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
diff --git a/lib/gitlab/danger/emoji_checker.rb b/lib/gitlab/danger/emoji_checker.rb
new file mode 100644
index 00000000000..e31a6ae5011
--- /dev/null
+++ b/lib/gitlab/danger/emoji_checker.rb
@@ -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
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 90cef384a1b..5363533ace5 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -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
diff --git a/lib/gitlab/error_tracking/repo.rb b/lib/gitlab/error_tracking/repo.rb
new file mode 100644
index 00000000000..50611943bac
--- /dev/null
+++ b/lib/gitlab/error_tracking/repo.rb
@@ -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
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 9a5e01462fb..43f3b673614 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -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)
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index beceed3fa75..c8932b26925 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -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
diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb
index 4893cbc1f45..3dda244233f 100644
--- a/lib/gitlab/sidekiq_middleware.rb
+++ b/lib/gitlab/sidekiq_middleware.rb
@@ -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
diff --git a/lib/gitlab/sidekiq_middleware/client_metrics.rb b/lib/gitlab/sidekiq_middleware/client_metrics.rb
new file mode 100644
index 00000000000..cd11415b55e
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/client_metrics.rb
@@ -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
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb
index 7bfb0d54d80..9588e9ef19a 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics.rb
@@ -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
diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb
new file mode 100644
index 00000000000..fa7f56b8d9c
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb
@@ -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
diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb
index 490f82c4678..8898960c24d 100644
--- a/lib/sentry/client.rb
+++ b/lib/sentry/client.rb
@@ -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
diff --git a/lib/sentry/client/issue_link.rb b/lib/sentry/client/issue_link.rb
new file mode 100644
index 00000000000..200b1a6b435
--- /dev/null
+++ b/lib/sentry/client/issue_link.rb
@@ -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
diff --git a/lib/sentry/client/repo.rb b/lib/sentry/client/repo.rb
new file mode 100644
index 00000000000..9a0ed3c7342
--- /dev/null
+++ b/lib/sentry/client/repo.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 15b16d3b43a..bff8e6d65f4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/public/robots.txt b/public/robots.txt
index f2ddb384ebb..130328f5687 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -72,4 +72,5 @@ Disallow: /*/*/protected_branches
Disallow: /*/*/uploads/
Disallow: /*/-/group_members
Disallow: /*/project_members
+Disallow: /groups/*/-/contribution_analytics
Disallow: /groups/*/-/analytics
diff --git a/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb b/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb
index 6ae37fe6d2f..1c29b68dc24 100644
--- a/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb
+++ b/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb
@@ -37,144 +37,72 @@ 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}"
+ expect(response).to have_gitlab_http_status :bad_request
+ expect(json_response).to eq('error' => "Request parameter #{param} is missing.")
+ end
+ end
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)
+ context "param branch_name is missing" do
+ let(:branch_name) { nil }
+ it 'responds with bad request status and error message', :aggregate_failures do
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 branch is missing.")
end
end
end
end
-
- context 'invalid dashboard template' do
- let(:dashboard) { 'config/database.yml' }
-
- it 'responds 404 not found' 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
- end
- end
end
context 'without rights to push to repository' do
diff --git a/spec/fixtures/sentry/issue_link_sample_response.json b/spec/fixtures/sentry/issue_link_sample_response.json
new file mode 100644
index 00000000000..f7f3220e83d
--- /dev/null
+++ b/spec/fixtures/sentry/issue_link_sample_response.json
@@ -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"
+}
diff --git a/spec/fixtures/sentry/repos_sample_response.json b/spec/fixtures/sentry/repos_sample_response.json
new file mode 100644
index 00000000000..fe389035fe3
--- /dev/null
+++ b/spec/fixtures/sentry/repos_sample_response.json
@@ -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"
+ }
+]
diff --git a/spec/frontend/issuable_suggestions/components/app_spec.js b/spec/frontend/issuable_suggestions/components/app_spec.js
index 7a2d2df78c4..20930be8667 100644
--- a/spec/frontend/issuable_suggestions/components/app_spec.js
+++ b/spec/frontend/issuable_suggestions/components/app_spec.js
@@ -11,7 +11,6 @@ describe('Issuable suggestions app component', () => {
search,
projectPath: 'project',
},
- attachToDocument: true,
});
}
diff --git a/spec/frontend/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index 4c89bb5fa81..6c3c30fcbb0 100644
--- a/spec/frontend/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
@@ -16,7 +16,6 @@ describe('Issuable suggestions suggestion component', () => {
...suggestion,
},
},
- attachToDocument: true,
});
}
diff --git a/spec/frontend/issuables_list/components/issuable_spec.js b/spec/frontend/issuables_list/components/issuable_spec.js
index b721fe61ace..81f6b60ae25 100644
--- a/spec/frontend/issuables_list/components/issuable_spec.js
+++ b/spec/frontend/issuables_list/components/issuable_spec.js
@@ -44,7 +44,6 @@ describe('Issuable component', () => {
baseUrl: TEST_BASE_URL,
...props,
},
- attachToDocument: true,
});
};
diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
index 3a01dc3a364..eafc4d83d87 100644
--- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
@@ -45,7 +45,6 @@ describe('Issuables list component', () => {
emptySvgPath: TEST_EMPTY_SVG_PATH,
...props,
},
- attachToDocument: true,
});
};
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index efae8b941ee..030b68bcd5a 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -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();
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
new file mode 100644
index 00000000000..6af5ab4ba75
--- /dev/null
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -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]]);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
new file mode 100644
index 00000000000..75a488b5c7b
--- /dev/null
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
@@ -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);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/init_utils.js b/spec/frontend/monitoring/init_utils.js
index 10db8b902b5..5f229cb6ee5 100644
--- a/spec/frontend/monitoring/init_utils.js
+++ b/spec/frontend/monitoring/init_utils.js
@@ -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',
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 77c92d0eca6..8ed0e232775 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -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',
},
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index c1ad59ac95b..975bdd3a27a 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -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();
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
index 78e086e473d..2902c8280dd 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
@@ -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');
}
});
}
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
index b48c97341b2..5e0f38459b0 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
@@ -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,
},
});
});
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index 3a52941a06e..02c4dabeffc 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -18,7 +18,6 @@ describe('Changed file icon', () => {
showTooltip: true,
...props,
},
- attachToDocument: true,
});
};
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index 233088b8d32..37f71867ab9 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -9,7 +9,6 @@ describe('clipboard button', () => {
const createWrapper = propsData => {
wrapper = shallowMount(ClipboardButton, {
propsData,
- attachToDocument: true,
});
};
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index 81c67b30a11..3510c9b699d 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -17,7 +17,6 @@ describe('Commit component', () => {
const createComponent = propsData => {
wrapper = shallowMount(CommitComponent, {
propsData,
- attachToDocument: true,
});
};
diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 00245c68342..b00261ae067 100644
--- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -17,7 +17,6 @@ describe('IssueAssigneesComponent', () => {
assignees: mockAssigneesList,
...props,
},
- attachToDocument: true,
});
vm = wrapper.vm; // eslint-disable-line
};
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
index ef752743fa9..4c654e01f74 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -13,7 +13,6 @@ const createComponent = (milestone = mockMilestone) => {
propsData: {
milestone,
},
- attachToDocument: true,
});
};
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index e895fe27095..f7b1f041ef2 100644
--- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -31,7 +31,6 @@ describe('RelatedIssuableItem', () => {
beforeEach(() => {
wrapper = mount(RelatedIssuableItem, {
slots,
- attachToDocument: true,
propsData: props,
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 8eec48749c9..551d781d296 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -12,7 +12,6 @@ describe('Markdown field header component', () => {
previewMarkdown: false,
...props,
},
- attachToDocument: true,
});
};
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index 0450166a468..9b9c3d559e3 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -17,7 +17,6 @@ describe('Suggestion Diff component', () => {
...DEFAULT_PROPS,
...props,
},
- attachToDocument: true,
});
};
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index 1c048560212..e5a8860f42e 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -16,7 +16,6 @@ describe('modal copy button', () => {
text: 'copy me',
title: 'Copy this value',
},
- attachToDocument: true,
});
});
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 91b68ce1c6f..d5eac7c2aa3 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -33,7 +33,6 @@ describe('system note component', () => {
vm = mount(IssueSystemNote, {
store,
propsData: props,
- attachToDocument: true,
});
});
diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js
index 3b064410274..46e45296c37 100644
--- a/spec/frontend/vue_shared/components/paginated_list_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_list_spec.js
@@ -26,7 +26,6 @@ describe('Pagination links component', () => {
list: [{ id: 'foo' }, { id: 'bar' }],
props,
},
- attachToDocument: true,
});
[glPaginatedList] = wrapper.vm.$children;
diff --git a/spec/frontend/vue_shared/components/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart_container_spec.js
index 552cfade7b6..3a5514ef318 100644
--- a/spec/frontend/vue_shared/components/resizable_chart_container_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart_container_spec.js
@@ -14,7 +14,6 @@ describe('Resizable Chart Container', () => {
beforeEach(() => {
wrapper = mount(ResizableChartContainer, {
- attachToDocument: true,
scopedSlots: {
default: `
<div class="slot" slot-scope="{ width, height }">
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
index 7dd1be24360..d90fafb6bf7 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -12,7 +12,6 @@ import {
const createComponent = (config = mockConfig) =>
shallowMount(BaseComponent, {
propsData: config,
- attachToDocument: true,
});
describe('BaseComponent', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 76f6ff96f82..54ad96073c8 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -24,7 +24,6 @@ const createComponent = (
labelFilterBasePath,
enableScopedLabels: true,
},
- attachToDocument: true,
});
};
diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
index a1db72c9c73..46fcb92455b 100644
--- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
@@ -7,7 +7,6 @@ describe('Time ago with tooltip component', () => {
const buildVm = (propsData = {}) => {
vm = shallowMount(TimeAgoTooltip, {
- attachToDocument: true,
propsData,
});
};
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index 2bbbab17bce..2f68e15b0d7 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -26,7 +26,6 @@ describe('User Avatar Link Component', () => {
...defaultProps,
...props,
},
- attachToDocument: true,
});
};
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index b1c9f8b505b..a8bbc80d2df 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -59,7 +59,6 @@ describe('User Popover Component', () => {
status: null,
},
},
- attachToDocument: true,
},
);
});
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
new file mode 100644
index 00000000000..cf30893b3ca
--- /dev/null
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -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
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 7bed1f72e0b..ac2d2d6f7f0 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -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
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index a50c8e9bf8e..b7a6cd4db74 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe EnvironmentsHelper do
- set(:environment) { create(:environment) }
- set(:project) { environment.project }
set(:user) { create(:user) }
+ set(:project) { create(:project, :repository) }
+ set(:environment) { create(:environment, project: project) }
describe '#metrics_data' do
before do
@@ -28,6 +28,7 @@ describe EnvironmentsHelper do
'empty-unable-to-connect-svg-path' => match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'),
'metrics-endpoint' => additional_metrics_project_environment_path(project, environment, format: :json),
'deployments-endpoint' => project_environment_deployments_path(project, environment, format: :json),
+ 'default-branch' => 'master',
'environments-endpoint': project_environments_path(project, format: :json),
'project-path' => project_path(project),
'tags-path' => project_tags_path(project),
diff --git a/spec/javascripts/monitoring/components/dashboard_resize_spec.js b/spec/javascripts/monitoring/components/dashboard_resize_spec.js
index 4eab398e3ab..46a6679da18 100644
--- a/spec/javascripts/monitoring/components/dashboard_resize_spec.js
+++ b/spec/javascripts/monitoring/components/dashboard_resize_spec.js
@@ -22,6 +22,7 @@ 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',
diff --git a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
index bd481f93413..242193c7b3d 100644
--- a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
@@ -8,7 +8,10 @@ describe('review app link', () => {
const props = {
link: '/review',
cssClass: 'js-link',
- isCurrent: true,
+ display: {
+ text: 'View app',
+ tooltip: '',
+ },
};
let vm;
let el;
diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb
index 689957993ec..64f87ec8cd3 100644
--- a/spec/lib/gitlab/danger/changelog_spec.rb
+++ b/spec/lib/gitlab/danger/changelog_spec.rb
@@ -106,18 +106,6 @@ describe Gitlab::Danger::Changelog do
end
end
- describe '#sanitized_mr_title' do
- subject { changelog.sanitized_mr_title }
-
- [
- 'WIP: My MR title',
- 'My MR title'
- ].each do |mr_title|
- let(:mr_json) { { "title" => mr_title } }
- it { is_expected.to eq("My MR title") }
- end
- end
-
describe '#ee_changelog?' do
context 'is ee changelog' do
[
diff --git a/spec/lib/gitlab/danger/commit_linter_spec.rb b/spec/lib/gitlab/danger/commit_linter_spec.rb
new file mode 100644
index 00000000000..0cf7ac64e43
--- /dev/null
+++ b/spec/lib/gitlab/danger/commit_linter_spec.rb
@@ -0,0 +1,315 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+require_relative 'danger_spec_helper'
+
+require 'gitlab/danger/commit_linter'
+
+describe Gitlab::Danger::CommitLinter do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:total_files_changed) { 2 }
+ let(:total_lines_changed) { 10 }
+ let(:stats) { { total: { files: total_files_changed, lines: total_lines_changed } } }
+ let(:diff_parent) { Struct.new(:stats).new(stats) }
+ let(:commit_class) do
+ Struct.new(:message, :sha, :diff_parent)
+ end
+ let(:commit_message) { 'A commit message' }
+ let(:commit_sha) { 'abcd1234' }
+ let(:commit) { commit_class.new(commit_message, commit_sha, diff_parent) }
+
+ subject(:commit_linter) { described_class.new(commit) }
+
+ describe '#fixup?' do
+ where(:commit_message, :is_fixup) do
+ 'A commit message' | false
+ 'fixup!' | true
+ 'fixup! A commit message' | true
+ 'squash!' | true
+ 'squash! A commit message' | true
+ end
+
+ with_them do
+ it 'is true when commit message starts with "fixup!" or "squash!"' do
+ expect(commit_linter.fixup?).to be(is_fixup)
+ end
+ end
+ end
+
+ describe '#suggestion?' do
+ where(:commit_message, :is_suggestion) do
+ 'A commit message' | false
+ 'Apply suggestion to' | true
+ 'Apply suggestion to "A commit message"' | true
+ end
+
+ with_them do
+ it 'is true when commit message starts with "Apply suggestion to"' do
+ expect(commit_linter.suggestion?).to be(is_suggestion)
+ end
+ end
+ end
+
+ describe '#merge?' do
+ where(:commit_message, :is_merge) do
+ 'A commit message' | false
+ 'Merge branch' | true
+ 'Merge branch "A commit message"' | true
+ end
+
+ with_them do
+ it 'is true when commit message starts with "Merge branch"' do
+ expect(commit_linter.merge?).to be(is_merge)
+ end
+ end
+ end
+
+ describe '#revert?' do
+ where(:commit_message, :is_revert) do
+ 'A commit message' | false
+ 'Revert' | false
+ 'Revert "' | true
+ 'Revert "A commit message"' | true
+ end
+
+ with_them do
+ it 'is true when commit message starts with "Revert \""' do
+ expect(commit_linter.revert?).to be(is_revert)
+ end
+ end
+ end
+
+ describe '#multi_line?' do
+ where(:commit_message, :is_multi_line) do
+ "A commit message" | false
+ "A commit message\n" | false
+ "A commit message\n\n" | false
+ "A commit message\n\nWith details" | true
+ end
+
+ with_them do
+ it 'is true when commit message contains details' do
+ expect(commit_linter.multi_line?).to be(is_multi_line)
+ end
+ end
+ end
+
+ describe '#failed?' do
+ context 'with no failures' do
+ it { expect(commit_linter).not_to be_failed }
+ end
+
+ context 'with failures' do
+ before do
+ commit_linter.add_problem(:details_line_too_long)
+ end
+
+ it { expect(commit_linter).to be_failed }
+ end
+ end
+
+ describe '#add_problem' do
+ it 'stores messages in #failures' do
+ commit_linter.add_problem(:details_line_too_long)
+
+ expect(commit_linter.problems).to eq({ details_line_too_long: described_class::PROBLEMS[:details_line_too_long] })
+ end
+ end
+
+ shared_examples 'a valid commit' do
+ it 'does not have any problem' do
+ commit_linter.lint
+
+ expect(commit_linter.problems).to be_empty
+ end
+ end
+
+ describe '#lint' do
+ describe 'subject' do
+ context 'when subject valid' do
+ it_behaves_like 'a valid commit'
+ end
+
+ context 'when subject is too short' do
+ let(:commit_message) { 'A B' }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class::DEFAULT_SUBJECT_DESCRIPTION)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when subject is too long' do
+ let(:commit_message) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class::DEFAULT_SUBJECT_DESCRIPTION)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when subject is too short and too long' do
+ let(:commit_message) { 'A ' + 'B' * described_class::MAX_LINE_LENGTH }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class::DEFAULT_SUBJECT_DESCRIPTION)
+ expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class::DEFAULT_SUBJECT_DESCRIPTION)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when subject is above warning' do
+ let(:commit_message) { 'A B ' + 'C' * described_class::WARN_SUBJECT_LENGTH }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_above_warning, described_class::DEFAULT_SUBJECT_DESCRIPTION)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when subject starts with lowercase' do
+ let(:commit_message) { 'a B C' }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class::DEFAULT_SUBJECT_DESCRIPTION)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when subject ands with a period' do
+ let(:commit_message) { 'A B C.' }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_ends_with_a_period, described_class::DEFAULT_SUBJECT_DESCRIPTION)
+
+ commit_linter.lint
+ end
+ end
+ end
+
+ describe 'separator' do
+ context 'when separator is missing' do
+ let(:commit_message) { "A B C\n" }
+
+ it_behaves_like 'a valid commit'
+ end
+
+ context 'when separator is a blank line' do
+ let(:commit_message) { "A B C\n\nMore details." }
+
+ it_behaves_like 'a valid commit'
+ end
+
+ context 'when separator is missing' do
+ let(:commit_message) { "A B C\nMore details." }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:separator_missing)
+
+ commit_linter.lint
+ end
+ end
+ end
+
+ describe 'details' do
+ context 'when details are valid' do
+ let(:commit_message) { "A B C\n\nMore details." }
+
+ it_behaves_like 'a valid commit'
+ end
+
+ context 'when no details are given and many files are changed' do
+ let(:total_files_changed) { described_class::MAX_CHANGED_FILES_IN_COMMIT + 1 }
+
+ it_behaves_like 'a valid commit'
+ end
+
+ context 'when no details are given and many lines are changed' do
+ let(:total_lines_changed) { described_class::MAX_CHANGED_LINES_IN_COMMIT + 1 }
+
+ it_behaves_like 'a valid commit'
+ end
+
+ context 'when no details are given and many files and lines are changed' do
+ let(:total_files_changed) { described_class::MAX_CHANGED_FILES_IN_COMMIT + 1 }
+ let(:total_lines_changed) { described_class::MAX_CHANGED_LINES_IN_COMMIT + 1 }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:details_too_many_changes)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when details exceeds the max line length' do
+ let(:commit_message) { "A B C\n\n" + 'D' * (described_class::MAX_LINE_LENGTH + 1) }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:details_line_too_long)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when details exceeds the max line length including a URL' do
+ let(:commit_message) { "A B C\n\nhttps://gitlab.com" + 'D' * described_class::MAX_LINE_LENGTH }
+
+ it_behaves_like 'a valid commit'
+ end
+ end
+
+ describe 'message' do
+ context 'when message includes a text emoji' do
+ let(:commit_message) { "A commit message :+1:" }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:message_contains_text_emoji)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when message includes a unicode emoji' do
+ let(:commit_message) { "A commit message 🚀" }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:message_contains_unicode_emoji)
+
+ commit_linter.lint
+ end
+ end
+
+ context 'when message includes a short reference' do
+ [
+ 'A commit message to fix #1234',
+ 'A commit message to fix !1234',
+ 'A commit message to fix &1234',
+ 'A commit message to fix %1234',
+ 'A commit message to fix gitlab#1234',
+ 'A commit message to fix gitlab!1234',
+ 'A commit message to fix gitlab&1234',
+ 'A commit message to fix gitlab%1234',
+ 'A commit message to fix gitlab-org/gitlab#1234',
+ 'A commit message to fix gitlab-org/gitlab!1234',
+ 'A commit message to fix gitlab-org/gitlab&1234',
+ 'A commit message to fix gitlab-org/gitlab%1234'
+ ].each do |message|
+ let(:commit_message) { message }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:message_contains_short_reference)
+
+ commit_linter.lint
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/emoji_checker_spec.rb b/spec/lib/gitlab/danger/emoji_checker_spec.rb
new file mode 100644
index 00000000000..0cdc18ce626
--- /dev/null
+++ b/spec/lib/gitlab/danger/emoji_checker_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require 'gitlab/danger/emoji_checker'
+
+describe Gitlab::Danger::EmojiChecker do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#includes_text_emoji?' do
+ where(:text, :includes_emoji) do
+ 'Hello World!' | false
+ ':+1:' | true
+ 'Hello World! :+1:' | true
+ end
+
+ with_them do
+ it 'is true when text includes a text emoji' do
+ expect(subject.includes_text_emoji?(text)).to be(includes_emoji)
+ end
+ end
+ end
+
+ describe '#includes_unicode_emoji?' do
+ where(:text, :includes_emoji) do
+ 'Hello World!' | false
+ '🚀' | true
+ 'Hello World! 🚀' | true
+ end
+
+ with_them do
+ it 'is true when text includes a text emoji' do
+ expect(subject.includes_unicode_emoji?(text)).to be(includes_emoji)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index edcd020a10f..ae0fcf443c5 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -313,6 +313,19 @@ describe Gitlab::Danger::Helper do
end
end
+ describe '#sanitize_mr_title' do
+ where(:mr_title, :expected_mr_title) do
+ 'My MR title' | 'My MR title'
+ 'WIP: My MR title' | 'My MR title'
+ end
+
+ with_them do
+ subject { helper.sanitize_mr_title(mr_title) }
+
+ it { is_expected.to eq(expected_mr_title) }
+ end
+ end
+
describe '#security_mr?' do
it 'returns false when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil)
diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index 3c26daba5a5..4b799c23de8 100644
--- a/spec/lib/gitlab/data_builder/note_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -137,7 +137,7 @@ describe Gitlab::DataBuilder::Note do
it 'returns the note and project snippet data' do
expect(data).to have_key(:snippet)
expect(data[:snippet].except('updated_at'))
- .to eq(snippet.reload.hook_attrs.except('updated_at'))
+ .to eq(snippet.hook_attrs.except('updated_at'))
expect(data[:snippet]['updated_at'])
.to be >= snippet.hook_attrs['updated_at']
end
diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
new file mode 100644
index 00000000000..d94abe613e8
--- /dev/null
+++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# We want to test Import on "complete" data set,
+# which means that every relation (as in our Import/Export definition) is covered.
+# Fixture JSONs we use for testing Import such as
+# `spec/fixtures/lib/gitlab/import_export/complex/project.json`
+# should include these relations being non-empty.
+describe 'Test coverage of the Project Import' do
+ include ConfigurationHelper
+
+ # `MUTED_RELATIONS` is a technical debt.
+ # This list expected to be empty or used as a workround
+ # in case this spec blocks an important urgent MR.
+ # It is also expected that adding a relation in the list should lead to
+ # opening a follow-up issue to fix this.
+ MUTED_RELATIONS = %w[
+ project.milestones.events.push_event_payload
+ project.issues.events.push_event_payload
+ project.issues.notes.events
+ project.issues.notes.events.push_event_payload
+ project.issues.milestone.events.push_event_payload
+ project.issues.issue_milestones
+ project.issues.issue_milestones.milestone
+ project.issues.resource_label_events.label.priorities
+ project.issues.designs.notes
+ project.issues.designs.notes.author
+ project.issues.designs.notes.events
+ project.issues.designs.notes.events.push_event_payload
+ project.merge_requests.metrics
+ project.merge_requests.notes.events.push_event_payload
+ project.merge_requests.events.push_event_payload
+ project.merge_requests.timelogs
+ project.merge_requests.label_links
+ project.merge_requests.label_links.label
+ project.merge_requests.label_links.label.priorities
+ project.merge_requests.milestone
+ project.merge_requests.milestone.events
+ project.merge_requests.milestone.events.push_event_payload
+ project.merge_requests.merge_request_milestones
+ project.merge_requests.merge_request_milestones.milestone
+ project.merge_requests.resource_label_events.label
+ project.merge_requests.resource_label_events.label.priorities
+ project.ci_pipelines.notes.events
+ project.ci_pipelines.notes.events.push_event_payload
+ project.protected_branches.unprotect_access_levels
+ project.prometheus_metrics
+ project.metrics_setting
+ project.boards.lists.label.priorities
+ project.service_desk_setting
+ ].freeze
+
+ # A list of JSON fixture files we use to test Import.
+ # Note that we use separate fixture to test ee-only features.
+ # Most of the relations are present in `complex/project.json`
+ # which is our main fixture.
+ PROJECT_JSON_FIXTURES = [
+ 'spec/fixtures/lib/gitlab/import_export/complex/project.json',
+ 'spec/fixtures/lib/gitlab/import_export/group/project.json',
+ 'spec/fixtures/lib/gitlab/import_export/light/project.json',
+ 'spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json',
+ 'ee/spec/fixtures/lib/gitlab/import_export/designs/project.json'
+ ].freeze
+
+ it 'ensures that all imported/exported relations are present in test JSONs' do
+ not_tested_relations = (relations_from_config - tested_relations) - MUTED_RELATIONS
+
+ expect(not_tested_relations).to be_empty, failure_message(not_tested_relations)
+ end
+
+ def relations_from_config
+ relation_paths_for(:project)
+ .map { |relation_names| relation_names.join(".") }
+ .to_set
+ end
+
+ def tested_relations
+ PROJECT_JSON_FIXTURES.flat_map(&method(:relations_from_json)).to_set
+ end
+
+ def relations_from_json(json_file)
+ json = ActiveSupport::JSON.decode(IO.read(json_file))
+
+ Gitlab::ImportExport::RelationRenameService.rename(json)
+
+ [].tap {|res| gather_relations({ project: json }, res, [])}
+ .map {|relation_names| relation_names.join('.')}
+ end
+
+ def gather_relations(item, res, path)
+ case item
+ when Hash
+ item.each do |k, v|
+ if (v.is_a?(Array) || v.is_a?(Hash)) && v.present?
+ new_path = path + [k]
+ res << new_path
+ gather_relations(v, res, new_path)
+ end
+ end
+ when Array
+ item.each {|i| gather_relations(i, res, path)}
+ end
+ end
+
+ def failure_message(not_tested_relations)
+ <<~MSG
+ These relations seem to be added recenty and
+ they expected to be covered in our Import specs: #{not_tested_relations}.
+
+ To do that, expand one of the files listed in `PROJECT_JSON_FIXTURES`
+ (or expand the list if you consider adding a new fixture file).
+
+ After that, add a new spec into
+ `spec/lib/gitlab/import_export/project_tree_restorer_spec.rb`
+ to check that the relation is being imported correctly.
+
+ In case the spec breaks the master or there is a sense of urgency,
+ you could include the relations into the `MUTED_RELATIONS` list.
+
+ Muting relations is considered to be a temporary solution, so please
+ open a follow-up issue and try to fix that when it is possible.
+ MSG
+ end
+end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index eb071d38eb7..5704f823b93 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -213,6 +213,10 @@ describe Gitlab::ImportExport::RelationFactory do
attr_accessor :service_id, :moved_to_id, :namespace_id, :ci_id, :random_project_id, :random_id, :milestone_id, :project_id
end
+ before do
+ allow(HazardousFooModel).to receive(:reflect_on_association).and_return(nil)
+ end
+
it 'does not preserve any foreign key IDs' do
expect(created_object.values).not_to include(99)
end
@@ -247,6 +251,10 @@ describe Gitlab::ImportExport::RelationFactory do
attr_accessor(*Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES)
end
+ before do
+ allow(ProjectFooModel).to receive(:reflect_on_association).and_return(nil)
+ end
+
it 'does not preserve any project foreign key IDs' do
expect(created_object.values).not_to include(99)
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
new file mode 100644
index 00000000000..6516016e67f
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::ClientMetrics do
+ context "with worker attribution" do
+ subject { described_class.new }
+
+ let(:queue) { :test }
+ let(:worker_class) { worker.class }
+ let(:job) { {} }
+ let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", latency_sensitive: "no" } }
+
+ shared_examples "a metrics client middleware" do
+ context "with mocked prometheus" do
+ let(:enqueued_jobs_metric) { double('enqueued jobs metric', increment: true) }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:counter).with(described_class::ENQUEUED, anything).and_return(enqueued_jobs_metric)
+ end
+
+ describe '#call' do
+ it 'yields block' do
+ expect { |b| subject.call(worker, job, :test, double, &b) }.to yield_control.once
+ end
+
+ it 'increments enqueued jobs metric' do
+ expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1)
+
+ subject.call(worker, job, :test, double) { nil }
+ end
+ end
+ end
+ end
+
+ context "when workers are not attributed" do
+ class TestNonAttributedWorker
+ include Sidekiq::Worker
+ end
+
+ it_behaves_like "a metrics client middleware" do
+ let(:worker) { TestNonAttributedWorker.new }
+ let(:labels) { default_labels }
+ end
+ end
+
+ context "when workers are attributed" do
+ def create_attributed_worker_class(latency_sensitive, external_dependencies, resource_boundary, category)
+ Class.new do
+ include Sidekiq::Worker
+ include WorkerAttributes
+
+ latency_sensitive_worker! if latency_sensitive
+ worker_has_external_dependencies! if external_dependencies
+ worker_resource_boundary resource_boundary unless resource_boundary == :unknown
+ feature_category category unless category.nil?
+ end
+ end
+
+ let(:latency_sensitive) { false }
+ let(:external_dependencies) { false }
+ let(:resource_boundary) { :unknown }
+ let(:feature_category) { nil }
+ let(:worker_class) { create_attributed_worker_class(latency_sensitive, external_dependencies, resource_boundary, feature_category) }
+ let(:worker) { worker_class.new }
+
+ context "latency sensitive" do
+ it_behaves_like "a metrics client middleware" do
+ let(:latency_sensitive) { true }
+ let(:labels) { default_labels.merge(latency_sensitive: "yes") }
+ end
+ end
+
+ context "external dependencies" do
+ it_behaves_like "a metrics client middleware" do
+ let(:external_dependencies) { true }
+ let(:labels) { default_labels.merge(external_dependencies: "yes") }
+ end
+ end
+
+ context "cpu boundary" do
+ it_behaves_like "a metrics client middleware" do
+ let(:resource_boundary) { :cpu }
+ let(:labels) { default_labels.merge(boundary: "cpu") }
+ end
+ end
+
+ context "memory boundary" do
+ it_behaves_like "a metrics client middleware" do
+ let(:resource_boundary) { :memory }
+ let(:labels) { default_labels.merge(boundary: "memory") }
+ end
+ end
+
+ context "feature category" do
+ it_behaves_like "a metrics client middleware" do
+ let(:feature_category) { :authentication }
+ let(:labels) { default_labels.merge(feature_category: "authentication") }
+ end
+ end
+
+ context "combined" do
+ it_behaves_like "a metrics client middleware" do
+ let(:latency_sensitive) { true }
+ let(:external_dependencies) { true }
+ let(:resource_boundary) { :cpu }
+ let(:feature_category) { :authentication }
+ let(:labels) { default_labels.merge(latency_sensitive: "yes", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index 36c6f377bde..65a961b34f8 100644
--- a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
-describe Gitlab::SidekiqMiddleware::Metrics do
+describe Gitlab::SidekiqMiddleware::ServerMetrics do
context "with worker attribution" do
subject { described_class.new }
diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb
index ef4a898bdb6..473d85c0143 100644
--- a/spec/lib/gitlab/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::SidekiqMiddleware do
Labkit::Middleware::Sidekiq::Server,
Gitlab::SidekiqMiddleware::InstrumentationLogger,
Gitlab::SidekiqStatus::ServerMiddleware,
- Gitlab::SidekiqMiddleware::Metrics,
+ Gitlab::SidekiqMiddleware::ServerMetrics,
Gitlab::SidekiqMiddleware::ArgumentsLogger,
Gitlab::SidekiqMiddleware::MemoryKiller,
Gitlab::SidekiqMiddleware::RequestStoreMiddleware
@@ -74,7 +74,7 @@ describe Gitlab::SidekiqMiddleware do
let(:request_store) { false }
let(:disabled_sidekiq_middlewares) do
[
- Gitlab::SidekiqMiddleware::Metrics,
+ Gitlab::SidekiqMiddleware::ServerMetrics,
Gitlab::SidekiqMiddleware::ArgumentsLogger,
Gitlab::SidekiqMiddleware::MemoryKiller,
Gitlab::SidekiqMiddleware::RequestStoreMiddleware
diff --git a/spec/lib/sentry/client/issue_link_spec.rb b/spec/lib/sentry/client/issue_link_spec.rb
new file mode 100644
index 00000000000..35a69be6de5
--- /dev/null
+++ b/spec/lib/sentry/client/issue_link_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Sentry::Client::IssueLink do
+ include SentryClientHelpers
+
+ let(:error_tracking_setting) { create(:project_error_tracking_setting, api_url: sentry_url) }
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:client) { error_tracking_setting.sentry_client }
+
+ let(:issue_link_sample_response) { JSON.parse(fixture_file('sentry/issue_link_sample_response.json')) }
+
+ describe '#create_issue_link' do
+ let(:integration_id) { 44444 }
+ let(:sentry_issue_id) { 11111111 }
+ let(:issue) { create(:issue, project: error_tracking_setting.project) }
+
+ let(:sentry_issue_link_url) { "https://sentrytest.gitlab.com/api/0/groups/#{sentry_issue_id}/integrations/#{integration_id}/" }
+ let(:sentry_api_response) { issue_link_sample_response }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :put, body: sentry_api_response, status: 201) }
+
+ subject { client.create_issue_link(integration_id, sentry_issue_id, issue) }
+
+ it_behaves_like 'calls sentry api'
+
+ it { is_expected.to be_present }
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_issue_link_url }
+
+ it_behaves_like 'no Sentry redirects', :put
+ end
+
+ context 'when exception is raised' do
+ let(:sentry_request_url) { sentry_issue_link_url }
+
+ it_behaves_like 'maps Sentry exceptions', :put
+ end
+ end
+end
diff --git a/spec/lib/sentry/client/repo_spec.rb b/spec/lib/sentry/client/repo_spec.rb
new file mode 100644
index 00000000000..7bc2811ef03
--- /dev/null
+++ b/spec/lib/sentry/client/repo_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Sentry::Client::Repo do
+ include SentryClientHelpers
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:token) { 'test-token' }
+ let(:client) { Sentry::Client.new(sentry_url, token) }
+ let(:repos_sample_response) { JSON.parse(fixture_file('sentry/repos_sample_response.json')) }
+
+ describe '#repos' do
+ let(:organization_slug) { 'gitlab' }
+ let(:sentry_repos_url) { "https://sentrytest.gitlab.com/api/0/organizations/#{organization_slug}/repos/" }
+ let(:sentry_api_response) { repos_sample_response }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_repos_url, body: sentry_api_response) }
+
+ subject { client.repos(organization_slug) }
+
+ it_behaves_like 'calls sentry api'
+
+ it { is_expected.to all( be_a(Gitlab::ErrorTracking::Repo)) }
+
+ it { expect(subject.length).to eq(1) }
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_repos_url }
+
+ it_behaves_like 'no Sentry redirects'
+ end
+
+ context 'when exception is raised' do
+ let(:sentry_request_url) { sentry_repos_url }
+
+ it_behaves_like 'maps Sentry exceptions'
+ end
+ end
+end
diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb
index 409e8be3198..e2da4564ca1 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/sentry/client_spec.rb
@@ -12,4 +12,6 @@ describe Sentry::Client do
it { is_expected.to respond_to :list_issues }
it { is_expected.to respond_to :issue_details }
it { is_expected.to respond_to :issue_latest_event }
+ it { is_expected.to respond_to :repos }
+ it { is_expected.to respond_to :create_issue_link }
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ce01765bb8c..7c20bb415e1 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1183,6 +1183,38 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe 'auto devops pipeline metrics' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:pipeline) { create(:ci_empty_pipeline, config_source: config_source) }
+ let(:config_source) { :auto_devops_source }
+
+ where(:action, :status) do
+ :succeed | 'success'
+ :drop | 'failed'
+ :skip | 'skipped'
+ :cancel | 'canceled'
+ end
+
+ with_them do
+ context "when pipeline receives action '#{params[:action]}'" do
+ subject { pipeline.public_send(action) }
+
+ it { expect { subject }.to change { auto_devops_pipelines_completed_total(status) }.by(1) }
+
+ context 'when not auto_devops_source?' do
+ let(:config_source) { :repository_source }
+
+ it { expect { subject }.not_to change { auto_devops_pipelines_completed_total(status) } }
+ end
+ end
+ end
+
+ def auto_devops_pipelines_completed_total(status)
+ Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status)
+ end
+ end
+
def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
create(:ci_build, *traits,
name: name,
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8ca167934f4..110f7a5af65 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -633,6 +633,27 @@ describe User, :do_not_mock_admin_mode do
end
end
end
+
+ describe '.active_without_ghosts' do
+ let_it_be(:user1) { create(:user, :external) }
+ let_it_be(:user2) { create(:user, state: 'blocked') }
+ let_it_be(:user3) { create(:user, ghost: true) }
+ let_it_be(:user4) { create(:user) }
+
+ it 'returns all active users but ghost users' do
+ expect(described_class.active_without_ghosts).to match_array([user1, user4])
+ end
+ end
+
+ describe '.without_ghosts' do
+ let_it_be(:user1) { create(:user, :external) }
+ let_it_be(:user2) { create(:user, state: 'blocked') }
+ let_it_be(:user3) { create(:user, ghost: true) }
+
+ it 'returns users without ghosts users' do
+ expect(described_class.without_ghosts).to match_array([user1, user2])
+ end
+ end
end
describe "Respond to" do
@@ -1252,7 +1273,7 @@ describe User, :do_not_mock_admin_mode do
let(:user) { double }
it 'filters by active users by default' do
- expect(described_class).to receive(:active).and_return([user])
+ expect(described_class).to receive(:active_without_ghosts).and_return([user])
expect(described_class.filter_items(nil)).to include user
end
diff --git a/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb b/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
new file mode 100644
index 00000000000..274d594fd68
--- /dev/null
+++ b/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::Dashboard::CloneDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :repository) }
+ set(:environment) { create(:environment, project: project) }
+
+ describe '#execute' do
+ subject(:service_call) { described_class.new(project, user, params).execute }
+
+ let(:commit_message) { 'test' }
+ let(:branch) { "dashboard_new_branch" }
+ let(:dashboard) { 'config/prometheus/common_metrics.yml' }
+ let(:file_name) { 'custom_dashboard.yml' }
+ let(:params) do
+ {
+ dashboard: dashboard,
+ file_name: file_name,
+ commit_message: commit_message,
+ branch: branch
+ }
+ end
+
+ let(:dashboard_attrs) do
+ {
+ commit_message: commit_message,
+ branch_name: branch,
+ start_branch: project.default_branch,
+ encoding: 'text',
+ file_path: ".gitlab/dashboards/#{file_name}",
+ file_content: File.read(dashboard)
+ }
+ end
+
+ context 'user does not have push right to repository' do
+ it_behaves_like 'misconfigured dashboard service response', :forbidden, %q(You can't commit to this project)
+ end
+
+ context 'with rights to push to the repository' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'wrong target file extension' do
+ let(:file_name) { 'custom_dashboard.txt' }
+
+ it_behaves_like 'misconfigured dashboard service response', :bad_request, 'The file name should have a .yml extension'
+ end
+
+ context 'wrong source dashboard file' do
+ let(:dashboard) { 'config/prometheus/common_metrics_123.yml' }
+
+ it_behaves_like 'misconfigured dashboard service response', :not_found, 'Not found.'
+ end
+
+ context 'path traversal attack attempt' do
+ let(:dashboard) { 'config/prometheus/../database.yml' }
+
+ it_behaves_like 'misconfigured dashboard service response', :not_found, 'Not found.'
+ end
+
+ context 'path traversal attack attempt on target file' do
+ let(:file_name) { '../../custom_dashboard.yml' }
+ let(:dashboard_attrs) do
+ {
+ commit_message: commit_message,
+ branch_name: branch,
+ start_branch: project.default_branch,
+ encoding: 'text',
+ file_path: ".gitlab/dashboards/custom_dashboard.yml",
+ file_content: File.read(dashboard)
+ }
+ end
+
+ it 'strips target file name to safe value', :aggregate_failures do
+ 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_call
+ end
+ end
+
+ context 'valid parameters' do
+ it 'delegates commit creation to Files::CreateService', :aggregate_failures do
+ 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_call
+ end
+
+ context 'selected branch already exists' do
+ let(:branch) { 'existing_branch' }
+
+ before do
+ project.repository.add_branch(user, branch, 'master')
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :bad_request, "There was an error creating the dashboard, branch named: existing_branch already exists."
+
+ # temporary not available function for first iteration
+ # follow up issue https://gitlab.com/gitlab-org/gitlab/issues/196237 which
+ # require this feature
+ # it 'pass correct params to Files::CreateService', :aggregate_failures do
+ # project.repository.add_branch(user, branch, 'master')
+ #
+ # 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_call
+ # end
+ end
+
+ context 'blank branch name' do
+ let(:branch) { '' }
+
+ it_behaves_like 'misconfigured dashboard service response', :bad_request, 'There was an error creating the dashboard, branch name is invalid.'
+ end
+
+ context 'dashboard file already exists' do
+ let(:branch) { 'custom_dashboard' }
+
+ before do
+ Files::CreateService.new(
+ project,
+ user,
+ commit_message: 'Create custom dashboard custom_dashboard.yml',
+ branch_name: 'master',
+ start_branch: 'master',
+ file_path: ".gitlab/dashboards/custom_dashboard.yml",
+ file_content: File.read('config/prometheus/common_metrics.yml')
+ ).execute
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :bad_request, "A file with 'custom_dashboard.yml' already exists in custom_dashboard branch"
+ end
+
+ it 'extends dashboard template path to absolute url' do
+ allow(::Files::CreateService).to receive(:new).and_return(double(execute: { status: :success }))
+
+ expect(File).to receive(:read).with(Rails.root.join('config/prometheus/common_metrics.yml')).and_return('')
+
+ service_call
+ end
+
+ context 'Files::CreateService success' do
+ before do
+ allow(::Files::CreateService).to receive(:new).and_return(double(execute: { status: :success }))
+ end
+
+ it 'clears dashboards cache' do
+ expect(project.repository).to receive(:refresh_method_caches).with([:metrics_dashboard])
+
+ service_call
+ end
+
+ it 'returns success', :aggregate_failures do
+ result = service_call
+ dashboard_details = {
+ path: '.gitlab/dashboards/custom_dashboard.yml',
+ display_name: 'custom_dashboard.yml',
+ default: false,
+ system_dashboard: false
+ }
+
+ expect(result[:status]).to be :success
+ expect(result[:http_status]).to be :created
+ expect(result[:dashboard]).to match dashboard_details
+ end
+ end
+
+ context 'Files::CreateService fails' do
+ before do
+ allow(::Files::CreateService).to receive(:new).and_return(double(execute: { status: :error }))
+ end
+
+ it 'does NOT clear dashboards cache' do
+ expect(project.repository).not_to receive(:refresh_method_caches)
+
+ service_call
+ end
+
+ it 'returns error' do
+ result = service_call
+ expect(result[:status]).to be :error
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb
index 5b425d0964d..908a3e1fb09 100644
--- a/spec/support/helpers/metrics_dashboard_helpers.rb
+++ b/spec/support/helpers/metrics_dashboard_helpers.rb
@@ -29,54 +29,4 @@ module MetricsDashboardHelpers
def business_metric_title
PrometheusMetricEnums.group_details[:business][:group_title]
end
-
- shared_examples_for 'misconfigured dashboard service response' do |status_code|
- it 'returns an appropriate message and status code' do
- result = service_call
-
- expect(result.keys).to contain_exactly(:message, :http_status, :status)
- expect(result[:status]).to eq(:error)
- expect(result[:http_status]).to eq(status_code)
- end
- end
-
- shared_examples_for 'valid dashboard service response for schema' do
- it 'returns a json representation of the dashboard' do
- result = service_call
-
- expect(result.keys).to contain_exactly(:dashboard, :status)
- expect(result[:status]).to eq(:success)
-
- expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
- end
- end
-
- shared_examples_for 'valid dashboard service response' do
- let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
-
- it_behaves_like 'valid dashboard service response for schema'
- end
-
- shared_examples_for 'caches the unprocessed dashboard for subsequent calls' do
- it do
- expect(YAML).to receive(:safe_load).once.and_call_original
-
- described_class.new(*service_params).get_dashboard
- described_class.new(*service_params).get_dashboard
- end
- end
-
- shared_examples_for 'valid embedded dashboard service response' do
- let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
-
- it_behaves_like 'valid dashboard service response for schema'
- end
-
- shared_examples_for 'raises error for users with insufficient permissions' do
- context 'when the user does not have sufficient access' do
- let(:user) { build(:user) }
-
- it_behaves_like 'misconfigured dashboard service response', :unauthorized
- end
- end
end
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index e079c32d6ae..1e2d11a66cb 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -116,9 +116,9 @@ RSpec.shared_examples "redis_shared_examples" do
clear_pool
end
- context 'when running not on sidekiq workers' do
+ context 'when running on single-threaded runtime' do
before do
- allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(false)
+ allow(Gitlab::Runtime).to receive(:multi_threaded?).and_return(false)
end
it 'instantiates a connection pool with size 5' do
@@ -128,10 +128,10 @@ RSpec.shared_examples "redis_shared_examples" do
end
end
- context 'when running on sidekiq workers' do
+ context 'when running on multi-threaded runtime' do
before do
- allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
- allow(Sidekiq).to receive(:options).and_return({ concurrency: 18 })
+ allow(Gitlab::Runtime).to receive(:multi_threaded?).and_return(true)
+ allow(Gitlab::Runtime).to receive(:max_threads).and_return(18)
end
it 'instantiates a connection pool with a size based on the concurrency of the worker' do
diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
index 76b71ebd3c5..4221708b55c 100644
--- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
+++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
@@ -10,7 +10,7 @@ RSpec.shared_examples 'calls sentry api' do
end
# Requires sentry_api_url and subject to be defined
-RSpec.shared_examples 'no Sentry redirects' do
+RSpec.shared_examples 'no Sentry redirects' do |http_method|
let(:redirect_to) { 'https://redirected.example.com' }
let(:other_url) { 'https://other.example.org' }
@@ -19,6 +19,7 @@ RSpec.shared_examples 'no Sentry redirects' do
let!(:redirect_req_stub) do
stub_sentry_request(
sentry_api_url,
+ http_method || :get,
status: 302,
headers: { location: redirect_to }
)
@@ -31,7 +32,7 @@ RSpec.shared_examples 'no Sentry redirects' do
end
end
-RSpec.shared_examples 'maps Sentry exceptions' do
+RSpec.shared_examples 'maps Sentry exceptions' do |http_method|
exceptions = {
Gitlab::HTTP::Error => 'Error when connecting to Sentry',
Net::OpenTimeout => 'Connection to Sentry timed out',
@@ -44,7 +45,10 @@ RSpec.shared_examples 'maps Sentry exceptions' do
exceptions.each do |exception, message|
context "#{exception}" do
before do
- stub_request(:get, sentry_request_url).to_raise(exception)
+ stub_request(
+ http_method || :get,
+ sentry_request_url
+ ).to_raise(exception)
end
it do
diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb
index eebed7e42c1..ed9964fa108 100644
--- a/spec/support/shared_examples/requests/api/status_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb
@@ -59,8 +59,9 @@ shared_examples_for '412 response' do
delete request, params: params, headers: { 'HTTP_IF_UNMODIFIED_SINCE' => '1990-01-12T00:00:48-0600' }
end
- it 'returns 412' do
+ it 'returns 412 with a JSON error' do
expect(response).to have_gitlab_http_status(412)
+ expect(json_response).to eq('message' => '412 Precondition Failed')
end
end
@@ -69,8 +70,9 @@ shared_examples_for '412 response' do
delete request, params: params, headers: { 'HTTP_IF_UNMODIFIED_SINCE' => Time.now }
end
- it 'returns accepted' do
+ it 'returns 204 with an empty body' do
expect(response).to have_gitlab_http_status(success_status)
+ expect(response.body).to eq('') if success_status == 204
end
end
end
diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
new file mode 100644
index 00000000000..30d91346df3
--- /dev/null
+++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+shared_examples_for 'misconfigured dashboard service response' do |status_code, message = nil|
+ it 'returns an appropriate message and status code', :aggregate_failures do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:message, :http_status, :status)
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(status_code)
+ expect(result[:message]).to eq(message) if message
+ end
+end
+
+shared_examples_for 'valid dashboard service response for schema' do
+ it 'returns a json representation of the dashboard' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:dashboard, :status)
+ expect(result[:status]).to eq(:success)
+
+ expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
+ end
+end
+
+shared_examples_for 'valid dashboard service response' do
+ let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+
+ it_behaves_like 'valid dashboard service response for schema'
+end
+
+shared_examples_for 'caches the unprocessed dashboard for subsequent calls' do
+ it do
+ expect(YAML).to receive(:safe_load).once.and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+end
+
+shared_examples_for 'valid embedded dashboard service response' do
+ let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
+
+ it_behaves_like 'valid dashboard service response for schema'
+end
+
+shared_examples_for 'raises error for users with insufficient permissions' do
+ context 'when the user does not have sufficient access' do
+ let(:user) { build(:user) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unauthorized
+ end
+end
diff --git a/vendor/elastic_stack/values.yaml b/vendor/elastic_stack/values.yaml
index 2aa254eaa4a..ccbff1ab38d 100644
--- a/vendor/elastic_stack/values.yaml
+++ b/vendor/elastic_stack/values.yaml
@@ -34,7 +34,35 @@ nginx-ldapauth-proxy:
enabled: false
elasticsearch-curator:
- enabled: false
+ enabled: true
+ configMaps:
+ config_yml: |-
+ ---
+ client:
+ hosts:
+ - elastic-stack-elasticsearch-client
+ port: 9200
+ action_file_yml: |-
+ ---
+ actions:
+ 1:
+ action: delete_indices
+ description: >-
+ Delete indices older than 15 days (based on index name), for filebeat-
+ prefixed indices. Ignore the error if the filter does not result in an
+ actionable list of indices (ignore_empty_list) and exit cleanly.
+ options:
+ ignore_empty_list: True
+ filters:
+ - filtertype: pattern
+ kind: prefix
+ value: filebeat-
+ - filtertype: age
+ source: name
+ direction: older
+ timestring: '%Y.%m.%d'
+ unit: days
+ unit_count: 15
elasticsearch-exporter:
enabled: false