Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/commons/jquery.js1
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js2
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js4
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js3
-rw-r--r--app/assets/javascripts/groups_select.js172
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue13
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js12
-rw-r--r--app/assets/javascripts/issuable_context.js12
-rw-r--r--app/assets/javascripts/issuable_form.js62
-rw-r--r--app/assets/javascripts/label_manager.js13
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js26
-rw-r--r--app/assets/javascripts/main.js30
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue44
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue329
-rw-r--r--app/assets/javascripts/monitoring/components/graph/axis.vue118
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue48
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue151
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue62
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue65
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_info.vue28
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue33
-rw-r--r--app/assets/javascripts/monitoring/event_hub.js3
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js86
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js4
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js42
-rw-r--r--app/assets/javascripts/monitoring/utils/measurements.js44
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js223
-rw-r--r--app/assets/javascripts/project_select.js174
-rw-r--r--app/assets/javascripts/project_select_combo_button.js10
-rw-r--r--app/assets/javascripts/users_select.js190
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue8
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss5
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
-rw-r--r--app/controllers/concerns/membership_actions.rb4
-rw-r--r--app/controllers/import/bitbucket_controller.rb4
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb21
-rw-r--r--app/controllers/projects/issues_controller.rb10
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb9
-rw-r--r--app/controllers/projects/pipelines_controller.rb7
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/projects/triggers_controller.rb7
-rw-r--r--app/finders/contributed_projects_finder.rb7
-rw-r--r--app/helpers/auth_helper.rb7
-rw-r--r--app/helpers/emails_helper.rb8
-rw-r--r--app/helpers/external_wiki_helper.rb12
-rw-r--r--app/helpers/members_helper.rb15
-rw-r--r--app/helpers/projects_helper.rb16
-rw-r--r--app/models/ci/build.rb51
-rw-r--r--app/models/ci/build_metadata.rb2
-rw-r--r--app/models/ci/trigger.rb3
-rw-r--r--app/models/commit.rb5
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/ci/metadatable.rb69
-rw-r--r--app/models/lfs_download_object.rb22
-rw-r--r--app/models/member.rb3
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/project.rb31
-rw-r--r--app/models/project_feature.rb19
-rw-r--r--app/models/project_team.rb12
-rw-r--r--app/models/user.rb8
-rw-r--r--app/policies/ci/pipeline_policy.rb9
-rw-r--r--app/policies/issue_policy.rb1
-rw-r--r--app/policies/note_policy.rb1
-rw-r--r--app/policies/personal_snippet_policy.rb5
-rw-r--r--app/policies/project_policy.rb24
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/presenters/ci/trigger_presenter.rb19
-rw-r--r--app/presenters/commit_presenter.rb13
-rw-r--r--app/presenters/merge_request_presenter.rb4
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/serializers/error_tracking/project_entity.rb7
-rw-r--r--app/serializers/error_tracking/project_serializer.rb7
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/services/members/destroy_service.rb28
-rw-r--r--app/services/notes/build_service.rb15
-rw-r--r--app/services/notification_service.rb3
-rw-r--r--app/services/projects/import_error_filter.rb14
-rw-r--r--app/services/projects/import_service.rb20
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb13
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb109
-rw-r--r--app/services/projects/update_pages_service.rb41
-rw-r--r--app/services/protected_branches/api_service.rb8
-rw-r--r--app/views/admin/runners/_runner.html.haml4
-rw-r--r--app/views/admin/runners/index.html.haml4
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml14
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml21
-rw-r--r--app/views/notify/_note_email.text.erb4
-rw-r--r--app/views/notify/autodevops_disabled_email.text.erb2
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.html.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.text.haml6
-rw-r--r--app/views/notify/issue_status_changed_email.html.haml2
-rw-r--r--app/views/notify/issue_status_changed_email.text.erb2
-rw-r--r--app/views/notify/member_access_requested_email.text.erb2
-rw-r--r--app/views/notify/member_invite_accepted_email.text.erb2
-rw-r--r--app/views/notify/member_invited_email.text.erb2
-rw-r--r--app/views/notify/merge_request_status_email.html.haml2
-rw-r--r--app/views/notify/merge_request_status_email.text.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml4
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml4
-rw-r--r--app/views/notify/new_gpg_key_email.html.haml2
-rw-r--r--app/views/notify/new_gpg_key_email.text.erb2
-rw-r--r--app/views/notify/new_issue_email.text.erb2
-rw-r--r--app/views/notify/new_mention_in_issue_email.text.erb4
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.text.erb4
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_ssh_key_email.html.haml2
-rw-r--r--app/views/notify/new_ssh_key_email.text.erb2
-rw-r--r--app/views/notify/new_user_email.html.haml2
-rw-r--r--app/views/notify/new_user_email.text.erb2
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb6
-rw-r--r--app/views/notify/pipeline_success_email.text.erb6
-rw-r--r--app/views/notify/push_to_merge_request_email.html.haml2
-rw-r--r--app/views/notify/push_to_merge_request_email.text.haml2
-rw-r--r--app/views/notify/reassigned_issue_email.html.haml2
-rw-r--r--app/views/notify/reassigned_issue_email.text.erb2
-rw-r--r--app/views/notify/reassigned_merge_request_email.html.haml4
-rw-r--r--app/views/notify/reassigned_merge_request_email.text.erb4
-rw-r--r--app/views/notify/resolved_all_discussions_email.html.haml2
-rw-r--r--app/views/notify/resolved_all_discussions_email.text.erb2
-rw-r--r--app/views/projects/blob/viewers/_readme.html.haml2
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/show.html.haml5
-rw-r--r--app/views/projects/commits/_commit.html.haml5
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml3
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/milestones/_form.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml33
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml3
-rw-r--r--app/views/projects/triggers/_trigger.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml10
-rw-r--r--app/views/projects/wikis/pages.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis_layout.html.haml2
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml6
-rw-r--r--app/views/shared/projects/_project.html.haml2
154 files changed, 1062 insertions, 1941 deletions
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index a7ed175f7a4..009153d0703 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -7,4 +7,3 @@ import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
import 'jquery.waitforimages';
-import 'select2/select2';
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 96dc1f07cb9..e81a1525df0 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -143,7 +143,7 @@ export default {
*/
created() {
this.service = new EnvironmentsService(this.endpoint);
- this.requestData = { page: this.page, scope: this.scope };
+ this.requestData = { page: this.page, scope: this.scope, nested: true };
this.poll = new Poll({
resource: this.service,
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 4e07ccba91a..cb4ff6856db 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -7,8 +7,8 @@ export default class EnvironmentsService {
}
fetchEnvironments(options = {}) {
- const { scope, page } = options;
- return axios.get(this.environmentsEndpoint, { params: { scope, page } });
+ const { scope, page, nested } = options;
+ return axios.get(this.environmentsEndpoint, { params: { scope, page, nested } });
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5808a2d4afa..ac9a31c202c 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -20,7 +20,8 @@ export default class EnvironmentsStore {
*
* Stores the received environments.
*
- * In the main environments endpoint, each environment has the following schema
+ * In the main environments endpoint (with { nested: true } in params), each folder
+ * has the following schema:
* { name: String, size: Number, latest: Object }
* In the endpoint to retrieve environments from each folder, the environment does
* not have the `latest` key and the data is all in the root level.
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 2049760fe29..bdadbb1bb2a 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -4,93 +4,97 @@ import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
export default function groupsSelect() {
- // Needs to be accessible in rspec
- window.GROUP_SELECT_PER_PAGE = 20;
- $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
- const $select = $(this);
- const allAvailable = $select.data('allAvailable');
- const skipGroups = $select.data('skipGroups') || [];
- const parentGroupID = $select.data('parentId');
- const groupsPath = parentGroupID
- ? Api.subgroupsPath.replace(':id', parentGroupID)
- : Api.groupsPath;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ // Needs to be accessible in rspec
+ window.GROUP_SELECT_PER_PAGE = 20;
+ $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
+ const $select = $(this);
+ const allAvailable = $select.data('allAvailable');
+ const skipGroups = $select.data('skipGroups') || [];
+ const parentGroupID = $select.data('parentId');
+ const groupsPath = parentGroupID
+ ? Api.subgroupsPath.replace(':id', parentGroupID)
+ : Api.groupsPath;
- $select.select2({
- placeholder: 'Search for a group',
- allowClear: $select.hasClass('allowClear'),
- multiple: $select.hasClass('multiselect'),
- minimumInputLength: 0,
- ajax: {
- url: Api.buildUrl(groupsPath),
- dataType: 'json',
- quietMillis: 250,
- transport(params) {
- axios[params.type.toLowerCase()](params.url, {
- params: params.data,
- })
- .then(res => {
- const results = res.data || [];
- const headers = normalizeHeaders(res.headers);
- const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
- const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
- const more = currentPage < totalPages;
+ $select.select2({
+ placeholder: 'Search for a group',
+ allowClear: $select.hasClass('allowClear'),
+ multiple: $select.hasClass('multiselect'),
+ minimumInputLength: 0,
+ ajax: {
+ url: Api.buildUrl(groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport(params) {
+ axios[params.type.toLowerCase()](params.url, {
+ params: params.data,
+ })
+ .then(res => {
+ const results = res.data || [];
+ const headers = normalizeHeaders(res.headers);
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
- params.success({
- results,
- pagination: {
- more,
- },
- });
- })
- .catch(params.error);
- },
- data(search, page) {
- return {
- search,
- page,
- per_page: window.GROUP_SELECT_PER_PAGE,
- all_available: allAvailable,
- };
- },
- results(data, page) {
- if (data.length) return { results: [] };
+ params.success({
+ results,
+ pagination: {
+ more,
+ },
+ });
+ })
+ .catch(params.error);
+ },
+ data(search, page) {
+ return {
+ search,
+ page,
+ per_page: window.GROUP_SELECT_PER_PAGE,
+ all_available: allAvailable,
+ };
+ },
+ results(data, page) {
+ if (data.length) return { results: [] };
- const groups = data.length ? data : data.results || [];
- const more = data.pagination ? data.pagination.more : false;
- const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
+ const groups = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+ const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
- return {
- results,
- page,
- more,
- };
- },
- },
- // eslint-disable-next-line consistent-return
- initSelection(element, callback) {
- const id = $(element).val();
- if (id !== '') {
- return Api.group(id, callback);
- }
- },
- formatResult(object) {
- return `<div class='group-result'> <div class='group-name'>${
- object.full_name
- }</div> <div class='group-path'>${object.full_path}</div> </div>`;
- },
- formatSelection(object) {
- return object.full_name;
- },
- dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
+ return {
+ results,
+ page,
+ more,
+ };
+ },
+ },
+ // eslint-disable-next-line consistent-return
+ initSelection(element, callback) {
+ const id = $(element).val();
+ if (id !== '') {
+ return Api.group(id, callback);
+ }
+ },
+ formatResult(object) {
+ return `<div class='group-result'> <div class='group-name'>${
+ object.full_name
+ }</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ },
+ formatSelection(object) {
+ return object.full_name;
+ },
+ dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
- $select.on('select2-loaded', () => {
- const dropdown = document.querySelector('.select2-infinite .select2-results');
- dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
- });
- });
+ $select.on('select2-loaded', () => {
+ const dropdown = document.querySelector('.select2-infinite .select2-results');
+ dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
+ });
+ });
+ })
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index f1d40586903..ce577ae85b0 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -107,16 +107,23 @@ export default {
class="commit-sha"
>{{ lastCommit.short_id }}</a
>
- by {{ lastCommit.author_name }}
+ by
+ <user-avatar-image
+ css-classes="ide-status-avatar"
+ :size="18"
+ :img-src="latestPipeline && latestPipeline.commit.author_gravatar_url"
+ :img-alt="lastCommit.author_name"
+ :tooltip-text="lastCommit.author_name"
+ />
+ {{ lastCommit.author_name }}
<time
v-tooltip
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
data-placement="top"
data-container="body"
+ >{{ lastCommitFormatedAge }}</time
>
- {{ lastCommitFormatedAge }}
- </time>
</div>
<div v-if="file" class="ide-status-file">{{ file.name }}</div>
<div v-if="file" class="ide-status-file">{{ file.eol }}</div>
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index 612c524ca1c..e0fb58ef195 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -11,10 +11,14 @@ class AutoWidthDropdownSelect {
init() {
const { dropdownClass } = this;
- this.$selectElement.select2({
- dropdownCssClass: dropdownClass,
- ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
- });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ this.$selectElement.select2({
+ dropdownCssClass: dropdownClass,
+ ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
+ });
+ })
+ .catch(() => {});
return this;
}
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index f3d722409b0..48e7ed1318d 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -7,10 +7,14 @@ export default class IssuableContext {
constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser);
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+ })
+ .catch(() => {});
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index c81a2230310..4d2533d01f1 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -120,35 +120,39 @@ export default class IssuableForm {
}
initTargetBranchDropdown() {
- this.$targetBranchSelect.select2({
- ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
- ajax: {
- url: this.$targetBranchSelect.data('endpoint'),
- dataType: 'JSON',
- quietMillis: 250,
- data(search) {
- return {
- search,
- };
- },
- results(data) {
- return {
- // `data` keys are translated so we can't just access them with a string based key
- results: data[Object.keys(data)[0]].map(name => ({
- id: name,
- text: name,
- })),
- };
- },
- },
- initSelection(el, callback) {
- const val = el.val();
-
- callback({
- id: val,
- text: val,
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ this.$targetBranchSelect.select2({
+ ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
+ ajax: {
+ url: this.$targetBranchSelect.data('endpoint'),
+ dataType: 'JSON',
+ quietMillis: 250,
+ data(search) {
+ return {
+ search,
+ };
+ },
+ results(data) {
+ return {
+ // `data` keys are translated so we can't just access them with a string based key
+ results: data[Object.keys(data)[0]].map(name => ({
+ id: name,
+ text: name,
+ })),
+ };
+ },
+ },
+ initSelection(el, callback) {
+ const val = el.val();
+
+ callback({
+ id: val,
+ text: val,
+ });
+ },
});
- },
- });
+ })
+ .catch(() => {});
}
}
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 062501d1d04..f134a54dd53 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -70,7 +70,18 @@ export default class LabelManager {
const $detachedLabel = $label.detach();
this.toggleLabelPriorityBadge($detachedLabel, action);
- $detachedLabel.appendTo($target);
+
+ const $labelEls = $target.find('li.label-list-item');
+
+ /*
+ * If there is a label element in the target, we'd want to
+ * append the new label just right next to it.
+ */
+ if ($labelEls.length) {
+ $labelEls.last().after($detachedLabel);
+ } else {
+ $detachedLabel.appendTo($target);
+ }
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 3b6a57dad44..ae8b4b4d635 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -614,10 +614,18 @@ export const spriteIcon = (icon, className = '') => {
/**
* This method takes in object with snake_case property names
- * and returns new object with camelCase property names
+ * and returns a new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
+ *
+ * This method also supports additional params in `options` object
+ *
+ * @param {Object} obj - Object to be converted.
+ * @param {Object} options - Object containing additional options.
+ * @param {boolean} options.deep - FLag to allow deep object converting
+ * @param {Array[]} dropKeys - List of properties to discard while building new object
+ * @param {Array[]} ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
*/
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
@@ -625,12 +633,26 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
}
const initial = Array.isArray(obj) ? [] : {};
+ const { deep = false, dropKeys = [], ignoreKeyNames = [] } = options;
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
const val = obj[prop];
- if (options.deep && (isObject(val) || Array.isArray(val))) {
+ // Drop properties from new object if
+ // there are any mentioned in options
+ if (dropKeys.indexOf(prop) > -1) {
+ return acc;
+ }
+
+ // Skip converting properties in new object
+ // if there are any mentioned in options
+ if (ignoreKeyNames.indexOf(prop) > -1) {
+ result[prop] = obj[prop];
+ return acc;
+ }
+
+ if (deep && (isObject(val) || Array.isArray(val))) {
result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
} else {
result[convertToCamelCase(prop)] = obj[prop];
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 4ba3543f9b2..8e10b3ad912 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -100,18 +100,24 @@ function deferredInitialisation() {
});
// Initialize select2 selects
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
-
- // Close select2 on escape
- $('.js-select2').on('select2-close', () => {
- setTimeout(() => {
- $('.select2-container-active').removeClass('select2-container-active');
- $(':focus').blur();
- }, 1);
- });
+ if ($('select.select2').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+
+ // Close select2 on escape
+ $('.js-select2').on('select2-close', () => {
+ setTimeout(() => {
+ $('.select2-container-active').removeClass('select2-container-active');
+ $(':focus').blur();
+ }, 1);
+ });
+ })
+ .catch(() => {});
+ }
// Initialize tooltips
$body.tooltip({
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index cea5c1a56ca..0b4bb9cc686 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -6,15 +6,12 @@ import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue';
-import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
-import eventHub from '../event_hub';
export default {
components: {
MonitorAreaChart,
- Graph,
GraphGroup,
EmptyState,
Icon,
@@ -25,21 +22,11 @@ export default {
required: false,
default: true,
},
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
showPanels: {
type: Boolean,
required: false,
default: true,
},
- forceSmallGraph: {
- type: Boolean,
- required: false,
- default: false,
- },
documentationPath: {
type: String,
required: true,
@@ -99,14 +86,10 @@ export default {
store: new MonitoringStore(),
state: 'gettingStarted',
showEmptyState: true,
- hoverData: {},
elWidth: 0,
};
},
computed: {
- graphComponent() {
- return gon.features && gon.features.areaChart ? MonitorAreaChart : Graph;
- },
forceRedraw() {
return this.elWidth;
},
@@ -122,10 +105,8 @@ export default {
childList: false,
subtree: false,
};
- eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
- eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
this.sidebarMutationObserver.disconnect();
},
@@ -176,9 +157,6 @@ export default {
resize() {
this.elWidth = this.$el.clientWidth;
},
- hoverChanged(data) {
- this.hoverData = data;
- },
},
};
</script>
@@ -196,13 +174,13 @@ export default {
class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"
>
<ul>
- <li v-for="environment in store.environmentsData" :key="environment.latest.id">
+ <li v-for="environment in store.environmentsData" :key="environment.id">
<a
- :href="environment.latest.metrics_path"
- :class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
+ :href="environment.metrics_path"
+ :class="{ 'is-active': environment.name == currentEnvironmentName }"
class="dropdown-item"
>
- {{ environment.latest.name }}
+ {{ environment.name }}
</a>
</li>
</ul>
@@ -215,23 +193,13 @@ export default {
:name="groupData.group"
:show-panels="showPanels"
>
- <component
- :is="graphComponent"
+ <monitor-area-chart
v-for="(graphData, graphIndex) in groupData.metrics"
:key="graphIndex"
:graph-data="graphData"
- :hover-data="hoverData"
- :deployment-data="store.deploymentData"
- :project-path="projectPath"
- :tags-path="tagsPath"
- :show-legend="showLegend"
- :small-graph="forceSmallGraph"
:alert-data="getGraphAlerts(graphData.id)"
group-id="monitor-area-chart"
- >
- <!-- EE content -->
- {{ null }}
- </component>
+ />
</graph-group>
</div>
<empty-state
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
deleted file mode 100644
index 309b73f5a4d..00000000000
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ /dev/null
@@ -1,329 +0,0 @@
-<script>
-import { scaleLinear, scaleTime } from 'd3-scale';
-import { axisLeft, axisBottom } from 'd3-axis';
-import _ from 'underscore';
-import { max, extent } from 'd3-array';
-import { select } from 'd3-selection';
-import GraphAxis from './graph/axis.vue';
-import GraphLegend from './graph/legend.vue';
-import GraphFlag from './graph/flag.vue';
-import GraphDeployment from './graph/deployment.vue';
-import GraphPath from './graph/path.vue';
-import MonitoringMixin from '../mixins/monitoring_mixins';
-import eventHub from '../event_hub';
-import measurements from '../utils/measurements';
-import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
-import createTimeSeries from '../utils/multiple_time_series';
-import bp from '../../breakpoints';
-
-const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
-
-export default {
- components: {
- GraphAxis,
- GraphFlag,
- GraphDeployment,
- GraphPath,
- GraphLegend,
- },
- mixins: [MonitoringMixin],
- props: {
- graphData: {
- type: Object,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- hoverData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- projectPath: {
- type: String,
- required: true,
- },
- tagsPath: {
- type: String,
- required: true,
- },
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
- smallGraph: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- baseGraphHeight: 450,
- baseGraphWidth: 600,
- graphHeight: 450,
- graphWidth: 600,
- graphHeightOffset: 120,
- margin: {},
- unitOfDisplay: '',
- yAxisLabel: '',
- legendTitle: '',
- reducedDeploymentData: [],
- measurements: measurements.large,
- currentData: {
- time: new Date(),
- value: 0,
- },
- currentXCoordinate: 0,
- currentCoordinates: {},
- showFlag: false,
- showFlagContent: false,
- timeSeries: [],
- graphDrawData: {},
- realPixelRatio: 1,
- seriesUnderMouse: [],
- };
- },
- computed: {
- outerViewBox() {
- return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
- },
- innerViewBox() {
- return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
- },
- axisTransform() {
- return `translate(70, ${this.graphHeight - 100})`;
- },
- paddingBottomRootSvg() {
- return {
- paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`,
- };
- },
- deploymentFlagData() {
- return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
- },
- shouldRenderData() {
- return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
- },
- },
- watch: {
- hoverData() {
- this.positionFlag();
- },
- },
- mounted() {
- this.draw();
- },
- methods: {
- showDot(path) {
- return this.showFlagContent && this.seriesUnderMouse.includes(path);
- },
- draw() {
- const breakpointSize = bp.getBreakpointSize();
- const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
-
- this.margin = measurements.large.margin;
-
- if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
- this.graphHeight = 300;
- this.margin = measurements.small.margin;
- this.measurements = measurements.small;
- }
-
- this.yAxisLabel = this.graphData.y_label || 'Values';
- this.graphWidth = svgWidth - this.margin.left - this.margin.right;
- this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
- this.baseGraphHeight = this.graphHeight - 50;
- this.baseGraphWidth = this.graphWidth;
-
- // pixel offsets inside the svg and outside are not 1:1
- this.realPixelRatio = svgWidth / this.baseGraphWidth;
-
- // set the legends on the axes
- const [query] = this.graphData.queries;
- this.legendTitle = query ? query.label : 'Average';
- this.unitOfDisplay = query ? query.unit : '';
-
- if (this.shouldRenderData) {
- this.renderAxesPaths();
- this.formatDeployments();
- }
- },
- handleMouseOverGraph(e) {
- let point = this.$refs.graphData.createSVGPoint();
- point.x = e.clientX;
- point.y = e.clientY;
- point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x += 7;
-
- this.seriesUnderMouse = this.timeSeries.filter(series => {
- const mouseX = series.timeSeriesScaleX.invert(point.x);
- let minDistance = Infinity;
-
- const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
- const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
- if (distance < minDistance) {
- minDistance = distance;
- return x;
- }
- return closest;
- });
-
- return series.values.find(v => v.time.toString() === closestTickMark);
- });
-
- const firstTimeSeries = this.seriesUnderMouse[0];
- const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
- const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
- const d0 = firstTimeSeries.values[overlayIndex - 1];
- const d1 = firstTimeSeries.values[overlayIndex];
- if (d0 === undefined || d1 === undefined) return;
- const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
- const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1;
- const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
- const currentDeployXPos = this.mouseOverDeployInfo(point.x);
-
- eventHub.$emit('hoverChanged', {
- hoveredDate,
- currentDeployXPos,
- });
- },
- renderAxesPaths() {
- ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries(
- this.graphData.queries,
- this.graphWidth,
- this.graphHeight,
- this.graphHeightOffset,
- ));
-
- if (_.findWhere(this.timeSeries, { renderCanary: true })) {
- this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
- }
-
- const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
- const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);
-
- const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
- axisXScale.domain(d3.extent(allValues, d => d.time));
- axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
-
- this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
- const seriesKeys = {};
- series.values.forEach(v => {
- seriesKeys[v.time] = true;
- });
- return {
- ...obj,
- ...seriesKeys,
- };
- }, {});
-
- const xAxis = d3
- .axisBottom()
- .scale(axisXScale)
- .ticks(this.graphWidth / 120)
- .tickFormat(timeScaleFormat);
-
- const yAxis = d3
- .axisLeft()
- .scale(axisYScale)
- .ticks(measurements.yTicks);
-
- d3.select(this.$refs.baseSvg)
- .select('.x-axis')
- .call(xAxis);
-
- const width = this.graphWidth;
- d3.select(this.$refs.baseSvg)
- .select('.y-axis')
- .call(yAxis)
- .selectAll('.tick')
- .each(function createTickLines(d, i) {
- if (i > 0) {
- d3.select(this)
- .select('line')
- .attr('x2', width)
- .attr('class', 'axis-tick');
- } // Avoid adding the class to the first tick, to prevent coloring
- }); // This will select all of the ticks once they're rendered
- },
- },
-};
-</script>
-
-<template>
- <div
- class="prometheus-graph"
- @mouseover="showFlagContent = true"
- @mouseleave="showFlagContent = false"
- >
- <div class="prometheus-graph-header">
- <h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div class="prometheus-graph-widgets"><slot></slot></div>
- </div>
- <div :style="paddingBottomRootSvg" class="prometheus-svg-container">
- <svg ref="baseSvg" :viewBox="outerViewBox">
- <g :transform="axisTransform" class="x-axis" />
- <g class="y-axis" transform="translate(70, 20)" />
- <graph-axis
- :graph-width="graphWidth"
- :graph-height="graphHeight"
- :margin="margin"
- :measurements="measurements"
- :y-axis-label="yAxisLabel"
- :unit-of-display="unitOfDisplay"
- />
- <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
- <slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
- <graph-path
- v-for="(path, index) in timeSeries"
- :key="index"
- :generated-line-path="path.linePath"
- :generated-area-path="path.areaPath"
- :line-style="path.lineStyle"
- :line-color="path.lineColor"
- :area-color="path.areaColor"
- :current-coordinates="currentCoordinates[path.metricTag]"
- :show-dot="showDot(path)"
- />
- <graph-deployment
- :deployment-data="reducedDeploymentData"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- />
- <rect
- ref="graphOverlay"
- :width="graphWidth - 70"
- :height="graphHeight - 100"
- class="prometheus-graph-overlay"
- transform="translate(-5, 20)"
- @mousemove="handleMouseOverGraph($event)"
- />
- </svg>
- <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
- <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
- {{ s__('Metrics|No data to display') }}
- </text>
- </svg>
- </svg>
- <graph-flag
- v-if="shouldRenderData"
- :real-pixel-ratio="realPixelRatio"
- :current-x-coordinate="currentXCoordinate"
- :current-data="currentData"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- :show-flag-content="showFlagContent"
- :time-series="seriesUnderMouse"
- :unit-of-display="unitOfDisplay"
- :legend-title="legendTitle"
- :deployment-flag-data="deploymentFlagData"
- :current-coordinates="currentCoordinates"
- />
- </div>
- <graph-legend v-if="showLegend" :legend-title="legendTitle" :time-series="timeSeries" />
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue
deleted file mode 100644
index 8f046857a20..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/axis.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<script>
-import { convertToSentenceCase } from '~/lib/utils/text_utility';
-import { s__ } from '~/locale';
-
-export default {
- props: {
- graphWidth: {
- type: Number,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- margin: {
- type: Object,
- required: true,
- },
- measurements: {
- type: Object,
- required: true,
- },
- yAxisLabel: {
- type: String,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- yLabelWidth: 0,
- yLabelHeight: 0,
- };
- },
- computed: {
- textTransform() {
- const yCoordinate =
- (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
-
- return `translate(15, ${yCoordinate}) rotate(-90)`;
- },
-
- rectTransform() {
- const yCoordinate =
- (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
- this.yLabelWidth / 2 || 0;
-
- return `translate(0, ${yCoordinate}) rotate(-90)`;
- },
-
- xPosition() {
- return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
- },
-
- yPosition() {
- return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
- },
-
- yAxisLabelSentenceCase() {
- return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`;
- },
-
- timeString() {
- return s__('PrometheusDashboard|Time');
- },
- },
- mounted() {
- this.$nextTick(() => {
- const bbox = this.$refs.ylabel.getBBox();
- this.yLabelWidth = bbox.width + 10; // Added some padding
- this.yLabelHeight = bbox.height + 5;
- });
- },
-};
-</script>
-<template>
- <g class="axis-label-container">
- <line
- :y1="yPosition"
- :x2="graphWidth + 20"
- :y2="yPosition"
- class="label-x-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- />
- <line
- :x2="10"
- :y2="yPosition"
- class="label-y-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- y1="0"
- />
- <rect
- :transform="rectTransform"
- :width="yLabelWidth"
- :height="yLabelHeight"
- class="rect-axis-text"
- />
- <text
- ref="ylabel"
- :transform="textTransform"
- class="label-axis-text y-label-text"
- text-anchor="middle"
- >
- {{ yAxisLabelSentenceCase }}
- </text>
- <rect :x="xPosition + 60" :y="graphHeight - 80" class="rect-axis-text" width="35" height="50" />
- <text :x="xPosition + 60" :y="yPosition" class="label-axis-text x-label-text" dy=".35em">
- {{ timeString }}
- </text>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
deleted file mode 100644
index bee9784692c..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-export default {
- props: {
- deploymentData: {
- type: Array,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- },
- computed: {
- calculatedHeight() {
- return this.graphHeight - this.graphHeightOffset;
- },
- },
- methods: {
- transformDeploymentGroup(deployment) {
- return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
- },
- },
-};
-</script>
-<template>
- <g class="deploy-info">
- <g
- v-for="(deployment, index) in deploymentData"
- :key="index"
- :transform="transformDeploymentGroup(deployment)"
- >
- <rect :height="calculatedHeight" x="0" y="0" width="3" fill="url(#shadow-gradient)" />
- <line :y2="calculatedHeight" class="deployment-line" x1="0" y1="0" x2="0" stroke="#000" />
- </g>
- <svg height="0" width="0">
- <defs>
- <linearGradient id="shadow-gradient">
- <stop offset="0%" stop-color="#000" stop-opacity="0.4" />
- <stop offset="100%" stop-color="#000" stop-opacity="0" />
- </linearGradient>
- </defs>
- </svg>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
deleted file mode 100644
index 9d6d1caef80..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<script>
-import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
-import { formatRelevantDigits } from '../../../lib/utils/number_utils';
-import Icon from '../../../vue_shared/components/icon.vue';
-import TrackLine from './track_line.vue';
-
-export default {
- components: {
- Icon,
- TrackLine,
- },
- props: {
- currentXCoordinate: {
- type: Number,
- required: true,
- },
- currentData: {
- type: Object,
- required: true,
- },
- deploymentFlagData: {
- type: Object,
- required: false,
- default: null,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- realPixelRatio: {
- type: Number,
- required: true,
- },
- showFlagContent: {
- type: Boolean,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- legendTitle: {
- type: String,
- required: true,
- },
- currentCoordinates: {
- type: Object,
- required: true,
- },
- },
- computed: {
- formatTime() {
- return this.deploymentFlagData
- ? timeFormat(this.deploymentFlagData.time)
- : timeFormat(this.currentData.time);
- },
- formatDate() {
- return this.deploymentFlagData
- ? dateFormat(this.deploymentFlagData.time)
- : dateFormat(this.currentData.time);
- },
- cursorStyle() {
- const xCoordinate = this.deploymentFlagData
- ? this.deploymentFlagData.xPos
- : this.currentXCoordinate;
-
- const offsetTop = 20 * this.realPixelRatio;
- const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
- const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
-
- return {
- top: `${offsetTop}px`,
- left: `${offsetLeft}px`,
- height: `${height}px`,
- };
- },
- flagOrientation() {
- if (this.currentXCoordinate * this.realPixelRatio > 120) {
- return 'left';
- }
- return 'right';
- },
- },
- methods: {
- seriesMetricValue(seriesIndex, series) {
- const indexFromCoordinates = this.currentCoordinates[series.metricTag]
- ? this.currentCoordinates[series.metricTag].currentDataIndex
- : 0;
- const index = this.deploymentFlagData
- ? this.deploymentFlagData.seriesIndex
- : indexFromCoordinates;
- const value = series.values[index] && series.values[index].value;
- if (Number.isNaN(value)) {
- return '-';
- }
- return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
- },
- seriesMetricLabel(index, series) {
- if (this.timeSeries.length < 2) {
- return this.legendTitle;
- }
- if (series.metricTag) {
- return series.metricTag;
- }
- return `series ${index + 1}`;
- },
- },
-};
-</script>
-
-<template>
- <div :style="cursorStyle" class="prometheus-graph-cursor">
- <div v-if="showFlagContent" :class="flagOrientation" class="prometheus-graph-flag popover">
- <div class="arrow-shadow"></div>
- <div class="arrow"></div>
- <div class="popover-title">
- <h5 v-if="deploymentFlagData">Deployed</h5>
- {{ formatDate }} <strong>{{ formatTime }}</strong>
- </div>
- <div v-if="deploymentFlagData" class="popover-content deploy-meta-content">
- <div>
- <icon :size="12" name="commit" />
- <a :href="deploymentFlagData.commitUrl"> {{ deploymentFlagData.sha.slice(0, 8) }} </a>
- </div>
- <div v-if="deploymentFlagData.tag">
- <icon :size="12" name="label" />
- <a :href="deploymentFlagData.tagUrl"> {{ deploymentFlagData.ref }} </a>
- </div>
- </div>
- <div class="popover-content">
- <table class="prometheus-table">
- <tr v-for="(series, index) in timeSeries" :key="index">
- <track-line :track="series" />
- <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
- <td>
- <strong>{{ seriesMetricValue(index, series) }}</strong>
- </td>
- </tr>
- </table>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
deleted file mode 100644
index b5211c306a3..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<script>
-import TrackLine from './track_line.vue';
-import TrackInfo from './track_info.vue';
-
-export default {
- components: {
- TrackLine,
- TrackInfo,
- },
- props: {
- legendTitle: {
- type: String,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- },
- methods: {
- isStable(track) {
- return {
- 'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
- };
- },
- },
-};
-</script>
-<template>
- <div class="prometheus-graph-legends prepend-left-10">
- <table class="prometheus-table">
- <tr
- v-for="(series, index) in timeSeries"
- v-if="series.shouldRenderLegend"
- :key="index"
- :class="isStable(series)"
- >
- <td>
- <strong v-if="series.renderCanary">{{ series.trackName }}</strong>
- </td>
- <track-line :track="series" />
- <td v-if="timeSeries.length > 1" class="legend-metric-title">
- <track-info v-if="series.metricTag" :track="series" />
- <track-info v-else :track="series">
- <strong>{{ legendTitle }}</strong> series {{ index + 1 }}
- </track-info>
- </td>
- <td v-else>
- <track-info :track="series">
- <strong>{{ legendTitle }}</strong>
- </track-info>
- </td>
- <template v-for="(track, trackIndex) in series.tracksLegend">
- <track-line :key="`track-line-${trackIndex}`" :track="track" />
- <td :key="`track-info-${trackIndex}`">
- <track-info :track="track" class="legend-metric-title" />
- </td>
- </template>
- </tr>
- </table>
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
deleted file mode 100644
index f2c237ec391..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-export default {
- props: {
- generatedLinePath: {
- type: String,
- required: true,
- },
- generatedAreaPath: {
- type: String,
- required: true,
- },
- lineStyle: {
- type: String,
- required: false,
- default: '',
- },
- lineColor: {
- type: String,
- required: true,
- },
- areaColor: {
- type: String,
- required: true,
- },
- currentCoordinates: {
- type: Object,
- required: false,
- default: () => ({ currentX: 0, currentY: 0 }),
- },
- showDot: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- strokeDashArray() {
- if (this.lineStyle === 'dashed') return '3, 1';
- if (this.lineStyle === 'dotted') return '1, 1';
- return null;
- },
- },
-};
-</script>
-<template>
- <g transform="translate(-5, 20)">
- <circle
- v-if="showDot"
- :cx="currentCoordinates.currentX"
- :cy="currentCoordinates.currentY"
- :fill="lineColor"
- :stroke="lineColor"
- class="circle-path"
- r="3"
- />
- <path :d="generatedAreaPath" :fill="areaColor" class="metric-area" />
- <path
- :d="generatedLinePath"
- :stroke="lineColor"
- :stroke-dasharray="strokeDashArray"
- class="metric-line"
- fill="none"
- stroke-width="1"
- />
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue
deleted file mode 100644
index 3464067834f..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/track_info.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import { formatRelevantDigits } from '~/lib/utils/number_utils';
-
-export default {
- name: 'TrackInfo',
- props: {
- track: {
- type: Object,
- required: true,
- },
- },
- computed: {
- summaryMetrics() {
- return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits(
- this.track.max,
- )}`;
- },
- },
-};
-</script>
-<template>
- <span>
- <slot>
- <strong> {{ track.metricTag }} </strong>
- </slot>
- {{ summaryMetrics }}
- </span>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
deleted file mode 100644
index d2ed1ba113e..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<script>
-export default {
- name: 'TrackLine',
- props: {
- track: {
- type: Object,
- required: true,
- },
- },
- computed: {
- stylizedLine() {
- if (this.track.lineStyle === 'dashed') return '6, 3';
- if (this.track.lineStyle === 'dotted') return '3, 3';
- return null;
- },
- },
-};
-</script>
-<template>
- <td>
- <svg width="16" height="8">
- <line
- :stroke-dasharray="stylizedLine"
- :stroke="track.lineColor"
- :x1="0"
- :x2="16"
- :y1="4"
- :y2="4"
- stroke-width="4"
- />
- </svg>
- </td>
-</template>
diff --git a/app/assets/javascripts/monitoring/event_hub.js b/app/assets/javascripts/monitoring/event_hub.js
deleted file mode 100644
index 0948c2e5352..00000000000
--- a/app/assets/javascripts/monitoring/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Vue from 'vue';
-
-export default new Vue();
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
deleted file mode 100644
index 87c3d969de4..00000000000
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { bisectDate } from '../utils/date_time_formatters';
-
-const mixins = {
- methods: {
- mouseOverDeployInfo(mouseXPos) {
- if (!this.reducedDeploymentData) return false;
-
- let dataFound = false;
- this.reducedDeploymentData = this.reducedDeploymentData.map(d => {
- const deployment = d;
- if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
- dataFound = d.xPos + 1;
-
- deployment.showDeploymentFlag = true;
- } else {
- deployment.showDeploymentFlag = false;
- }
- return deployment;
- });
-
- return dataFound;
- },
-
- formatDeployments() {
- this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
- const time = new Date(deployment.created_at);
- const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time));
-
- time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
-
- if (xPos >= 0) {
- const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
-
- deploymentDataArray.push({
- id: deployment.id,
- time,
- sha: deployment.sha,
- commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
- tag: deployment.tag,
- tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
- ref: deployment.ref.name,
- xPos,
- seriesIndex,
- showDeploymentFlag: false,
- });
- }
-
- return deploymentDataArray;
- }, []);
- },
-
- positionFlag() {
- const timeSeries = this.seriesUnderMouse[0];
- if (!timeSeries) {
- return;
- }
- const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
-
- this.currentData = timeSeries.values[hoveredDataIndex];
- this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
-
- this.currentCoordinates = {};
-
- this.seriesUnderMouse.forEach(series => {
- const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
- const currentData = series.values[currentDataIndex];
- const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
- const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
-
- this.currentCoordinates[series.metricTag] = {
- currentX,
- currentY,
- currentDataIndex,
- };
- });
-
- if (this.hoverData.currentDeployXPos) {
- this.showFlag = false;
- } else {
- this.showFlag = true;
- }
- },
- },
-};
-
-export default mixins;
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 8692c873a41..96ecc5ab8a8 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -66,9 +66,7 @@ export default class MonitoringStore {
}
storeEnvironmentsData(environmentsData = []) {
- this.environmentsData = environmentsData.filter(
- environment => !!environment.latest.last_deployment,
- );
+ this.environmentsData = environmentsData.filter(environment => !!environment.last_deployment);
}
getMetricsCount() {
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
deleted file mode 100644
index d88c13609dc..00000000000
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { timeFormat as time } from 'd3-time-format';
-import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time';
-import { bisector } from 'd3-array';
-
-const d3 = {
- time,
- bisector,
- timeSecond,
- timeMinute,
- timeHour,
- timeDay,
- timeWeek,
- timeMonth,
- timeYear,
-};
-
-export const dateFormat = d3.time('%d %b %Y, ');
-export const timeFormat = d3.time('%-I:%M%p');
-export const dateFormatWithName = d3.time('%a, %b %-d');
-export const bisectDate = d3.bisector(d => d.time).left;
-
-export function timeScaleFormat(date) {
- let formatFunction;
- if (d3.timeSecond(date) < date) {
- formatFunction = d3.time('.%L');
- } else if (d3.timeMinute(date) < date) {
- formatFunction = d3.time(':%S');
- } else if (d3.timeHour(date) < date) {
- formatFunction = d3.time('%-I:%M');
- } else if (d3.timeDay(date) < date) {
- formatFunction = d3.time('%-I %p');
- } else if (d3.timeWeek(date) < date) {
- formatFunction = d3.time('%a %d');
- } else if (d3.timeMonth(date) < date) {
- formatFunction = d3.time('%b %d');
- } else if (d3.timeYear(date) < date) {
- formatFunction = d3.time('%B');
- } else {
- formatFunction = d3.time('%Y');
- }
- return formatFunction(date);
-}
diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js
deleted file mode 100644
index 7c771f43eee..00000000000
--- a/app/assets/javascripts/monitoring/utils/measurements.js
+++ /dev/null
@@ -1,44 +0,0 @@
-export default {
- small: {
- // Covers both xs and sm screen sizes
- margin: {
- top: 40,
- right: 40,
- bottom: 50,
- left: 40,
- },
- legends: {
- width: 15,
- height: 3,
- offsetX: 20,
- offsetY: 32,
- },
- backgroundLegend: {
- width: 30,
- height: 50,
- },
- axisLabelLineOffset: -20,
- },
- large: {
- // This covers both md and lg screen sizes
- margin: {
- top: 80,
- right: 80,
- bottom: 100,
- left: 80,
- },
- legends: {
- width: 15,
- height: 3,
- offsetX: 20,
- offsetY: 34,
- },
- backgroundLegend: {
- width: 30,
- height: 150,
- },
- axisLabelLineOffset: 20,
- },
- xTicks: 8,
- yTicks: 3,
-};
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
deleted file mode 100644
index 50ba14dfb2e..00000000000
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ /dev/null
@@ -1,223 +0,0 @@
-import _ from 'underscore';
-import { scaleLinear, scaleTime } from 'd3-scale';
-import { line, area, curveLinear } from 'd3-shape';
-import { extent, max, sum } from 'd3-array';
-import { timeMinute, timeSecond } from 'd3-time';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-
-const d3 = {
- scaleLinear,
- scaleTime,
- line,
- area,
- curveLinear,
- extent,
- max,
- timeMinute,
- timeSecond,
- sum,
-};
-
-const defaultColorPalette = {
- blue: ['#1f78d1', '#8fbce8'],
- orange: ['#fc9403', '#feca81'],
- red: ['#db3b21', '#ed9d90'],
- green: ['#1aaa55', '#8dd5aa'],
- purple: ['#6666c4', '#d1d1f0'],
-};
-
-const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
-
-const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
-
-function queryTimeSeries(query, graphDrawData, lineStyle) {
- let usedColors = [];
- let renderCanary = false;
- const timeSeriesParsed = [];
-
- function pickColor(name) {
- let pick;
- if (name && defaultColorPalette[name]) {
- pick = name;
- } else {
- const unusedColors = _.difference(defaultColorOrder, usedColors);
- if (unusedColors.length > 0) {
- [pick] = unusedColors;
- } else {
- usedColors = [];
- [pick] = defaultColorOrder;
- }
- }
- usedColors.push(pick);
- return defaultColorPalette[pick];
- }
-
- function findByDate(series, time) {
- const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
- if (val) {
- return val.value;
- }
- return NaN;
- }
-
- // The timeseries data may have gaps in it
- // but we need a regularly-spaced set of time/value pairs
- // this gives us a complete range of one minute intervals
- // offset the same amount as the original data
- const [minX, maxX] = graphDrawData.xDom;
- const offset = d3.timeMinute(minX) - Number(minX);
- const datesWithoutGaps = d3.timeSecond
- .every(60)
- .range(d3.timeMinute.offset(minX, -1), maxX)
- .map(d => d - offset);
-
- query.result.forEach((timeSeries, timeSeriesNumber) => {
- let metricTag = '';
- let lineColor = '';
- let areaColor = '';
- let shouldRenderLegend = true;
- const timeSeriesValues = timeSeries.values.map(d => d.value);
- const maximumValue = d3.max(timeSeriesValues);
- const accum = d3.sum(timeSeriesValues);
- const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable');
-
- if (trackName === 'Canary') {
- renderCanary = true;
- }
-
- const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
- const seriesCustomizationData =
- query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
-
- if (seriesCustomizationData) {
- metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
- [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
- if (timeSeriesParsed.length > 0) {
- shouldRenderLegend = false;
- } else {
- shouldRenderLegend = true;
- }
- } else {
- metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
- [lineColor, areaColor] = pickColor();
- if (timeSeriesParsed.length > 1) {
- shouldRenderLegend = false;
- }
- }
-
- const values = datesWithoutGaps.map(time => ({
- time,
- value: findByDate(timeSeries.values, time),
- }));
-
- timeSeriesParsed.push({
- linePath: graphDrawData.lineFunction(values),
- areaPath: graphDrawData.areaBelowLine(values),
- timeSeriesScaleX: graphDrawData.timeSeriesScaleX,
- timeSeriesScaleY: graphDrawData.timeSeriesScaleY,
- values: timeSeries.values,
- max: maximumValue,
- average: accum / timeSeries.values.length,
- lineStyle,
- lineColor,
- areaColor,
- metricTag,
- trackName,
- shouldRenderLegend,
- renderCanary,
- });
-
- if (!shouldRenderLegend) {
- if (!timeSeriesParsed[0].tracksLegend) {
- timeSeriesParsed[0].tracksLegend = [];
- }
- timeSeriesParsed[0].tracksLegend.push({
- max: maximumValue,
- average: accum / timeSeries.values.length,
- lineStyle,
- lineColor,
- metricTag,
- });
- }
- });
-
- return timeSeriesParsed;
-}
-
-function xyDomain(queries) {
- const allValues = queries.reduce(
- (allQueryResults, query) =>
- allQueryResults.concat(
- query.result.reduce((allResults, result) => allResults.concat(result.values), []),
- ),
- [],
- );
-
- const xDom = d3.extent(allValues, d => d.time);
- const yDom = [0, d3.max(allValues.map(d => d.value))];
-
- return {
- xDom,
- yDom,
- };
-}
-
-export function generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset) {
- const { xDom, yDom } = xyDomain(queries);
-
- const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
- const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
-
- timeSeriesScaleX.domain(xDom);
- timeSeriesScaleX.ticks(d3.timeMinute, 60);
- timeSeriesScaleY.domain(yDom);
-
- const defined = d => !Number.isNaN(d.value) && d.value != null;
-
- const lineFunction = d3
- .line()
- .defined(defined)
- .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
- .x(d => timeSeriesScaleX(d.time))
- .y(d => timeSeriesScaleY(d.value));
-
- const areaBelowLine = d3
- .area()
- .defined(defined)
- .curve(d3.curveLinear)
- .x(d => timeSeriesScaleX(d.time))
- .y0(graphHeight - graphHeightOffset)
- .y1(d => timeSeriesScaleY(d.value));
-
- const areaAboveLine = d3
- .area()
- .defined(defined)
- .curve(d3.curveLinear)
- .x(d => timeSeriesScaleX(d.time))
- .y0(0)
- .y1(d => timeSeriesScaleY(d.value));
-
- return {
- lineFunction,
- areaBelowLine,
- areaAboveLine,
- xDom,
- yDom,
- timeSeriesScaleX,
- timeSeriesScaleY,
- };
-}
-
-export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
- const graphDrawData = generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset);
-
- const timeSeries = queries.reduce((series, query, index) => {
- const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
- return series.concat(queryTimeSeries(query, graphDrawData, lineStyle));
- }, []);
-
- return {
- timeSeries,
- graphDrawData,
- };
-}
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index a33835472bb..5ee510eb11d 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -5,97 +5,101 @@ import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
export default function projectSelect() {
- $('.ajax-project-select').each(function(i, select) {
- var placeholder;
- const simpleFilter = $(select).data('simpleFilter') || false;
- this.groupId = $(select).data('groupId');
- this.includeGroups = $(select).data('includeGroups');
- this.allProjects = $(select).data('allProjects') || false;
- this.orderBy = $(select).data('orderBy') || 'id';
- this.withIssuesEnabled = $(select).data('withIssuesEnabled');
- this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
- this.withShared =
- $(select).data('withShared') === undefined ? true : $(select).data('withShared');
- this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
- this.allowClear = $(select).data('allowClear') || false;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-project-select').each(function(i, select) {
+ var placeholder;
+ const simpleFilter = $(select).data('simpleFilter') || false;
+ this.groupId = $(select).data('groupId');
+ this.includeGroups = $(select).data('includeGroups');
+ this.allProjects = $(select).data('allProjects') || false;
+ this.orderBy = $(select).data('orderBy') || 'id';
+ this.withIssuesEnabled = $(select).data('withIssuesEnabled');
+ this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
+ this.withShared =
+ $(select).data('withShared') === undefined ? true : $(select).data('withShared');
+ this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
+ this.allowClear = $(select).data('allowClear') || false;
- placeholder = 'Search for project';
- if (this.includeGroups) {
- placeholder += ' or group';
- }
+ placeholder = 'Search for project';
+ if (this.includeGroups) {
+ placeholder += ' or group';
+ }
- $(select).select2({
- placeholder: placeholder,
- minimumInputLength: 0,
- query: (function(_this) {
- return function(query) {
- var finalCallback, projectsCallback;
- finalCallback = function(projects) {
- var data;
- data = {
- results: projects,
- };
- return query.callback(data);
- };
- if (_this.includeGroups) {
- projectsCallback = function(projects) {
- var groupsCallback;
- groupsCallback = function(groups) {
+ $(select).select2({
+ placeholder: placeholder,
+ minimumInputLength: 0,
+ query: (function(_this) {
+ return function(query) {
+ var finalCallback, projectsCallback;
+ finalCallback = function(projects) {
var data;
- data = groups.concat(projects);
- return finalCallback(data);
+ data = {
+ results: projects,
+ };
+ return query.callback(data);
};
- return Api.groups(query.term, {}, groupsCallback);
+ if (_this.includeGroups) {
+ projectsCallback = function(projects) {
+ var groupsCallback;
+ groupsCallback = function(groups) {
+ var data;
+ data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(query.term, {}, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (_this.groupId) {
+ return Api.groupProjects(
+ _this.groupId,
+ query.term,
+ {
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ with_shared: _this.withShared,
+ include_subgroups: _this.includeProjectsInSubgroups,
+ },
+ projectsCallback,
+ );
+ } else {
+ return Api.projects(
+ query.term,
+ {
+ order_by: _this.orderBy,
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ membership: !_this.allProjects,
+ },
+ projectsCallback,
+ );
+ }
};
- } else {
- projectsCallback = finalCallback;
- }
- if (_this.groupId) {
- return Api.groupProjects(
- _this.groupId,
- query.term,
- {
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- with_shared: _this.withShared,
- include_subgroups: _this.includeProjectsInSubgroups,
- },
- projectsCallback,
- );
- } else {
- return Api.projects(
- query.term,
- {
- order_by: _this.orderBy,
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- membership: !_this.allProjects,
- },
- projectsCallback,
- );
- }
- };
- })(this),
- id: function(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text: function(project) {
- return project.name_with_namespace || project.name;
- },
+ })(this),
+ id: function(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
+ },
+ text: function(project) {
+ return project.name_with_namespace || project.name;
+ },
- initSelection: function(el, callback) {
- return Api.project(el.val()).then(({ data }) => callback(data));
- },
+ initSelection: function(el, callback) {
+ return Api.project(el.val()).then(({ data }) => callback(data));
+ },
- allowClear: this.allowClear,
+ allowClear: this.allowClear,
- dropdownCssClass: 'ajax-project-dropdown',
- });
- if (simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
+ dropdownCssClass: 'ajax-project-dropdown',
+ });
+ if (simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+ })
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index 3dbac3ff942..d3b5f532dc1 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -44,9 +44,13 @@ export default class ProjectSelectComboButton {
// eslint-disable-next-line class-methods-use-this
openDropdown(event) {
- $(event.currentTarget)
- .siblings('.project-item-select')
- .select2('open');
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $(event.currentTarget)
+ .siblings('.project-item-select')
+ .select2('open');
+ })
+ .catch(() => {});
}
selectProject() {
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index ce051582299..4017630d6ef 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -579,101 +579,109 @@ function UsersSelect(currentUser, els, options = {}) {
};
})(this),
);
- $('.ajax-users-select').each(
- (function(_this) {
- return function(i, select) {
- var firstUser, showAnyUser, showEmailUser, showNullUser;
- var options = {};
- options.skipLdap = $(select).hasClass('skip_ldap');
- options.projectId = $(select).data('projectId');
- options.groupId = $(select).data('groupId');
- options.showCurrentUser = $(select).data('currentUser');
- options.authorId = $(select).data('authorId');
- options.skipUsers = $(select).data('skipUsers');
- showNullUser = $(select).data('nullUser');
- showAnyUser = $(select).data('anyUser');
- showEmailUser = $(select).data('emailUser');
- firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: 'Search for a user',
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query: function(query) {
- return _this.users(query.term, options, function(users) {
- var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
- data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- ref = data.results;
-
- for (index = 0, len = ref.length; index < len; index += 1) {
- obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-users-select').each(
+ (function(_this) {
+ return function(i, select) {
+ var firstUser, showAnyUser, showEmailUser, showNullUser;
+ var options = {};
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ options.projectId = $(select).data('projectId');
+ options.groupId = $(select).data('groupId');
+ options.showCurrentUser = $(select).data('currentUser');
+ options.authorId = $(select).data('authorId');
+ options.skipUsers = $(select).data('skipUsers');
+ showNullUser = $(select).data('nullUser');
+ showAnyUser = $(select).data('anyUser');
+ showEmailUser = $(select).data('emailUser');
+ firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: 'Search for a user',
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ return _this.users(query.term, options, function(users) {
+ var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
+ data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ ref = data.results;
+
+ for (index = 0, len = ref.length; index < len; index += 1) {
+ obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ nullUser = {
+ name: 'Unassigned',
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = 'Any User';
+ }
+ anyUser = {
+ name: name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
}
}
- }
- if (showNullUser) {
- nullUser = {
- name: 'Unassigned',
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = 'Any User';
+ if (
+ showEmailUser &&
+ data.results.length === 0 &&
+ query.term.match(/^[^@]+@[^@]+$/)
+ ) {
+ var trimmed = query.term.trim();
+ emailUser = {
+ name: 'Invite "' + trimmed + '" by email',
+ username: trimmed,
+ id: trimmed,
+ invite: true,
+ };
+ data.results.unshift(emailUser);
}
- anyUser = {
- name: name,
- id: null,
- };
- data.results.unshift(anyUser);
- }
- }
- if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
- var trimmed = query.term.trim();
- emailUser = {
- name: 'Invite "' + trimmed + '" by email',
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
+ return query.callback(data);
+ });
+ },
+ initSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.initSelection.apply(_this, args);
+ },
+ formatResult: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.formatResult.apply(_this, args);
+ },
+ formatSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.formatSelection.apply(_this, args);
+ },
+ dropdownCssClass: 'ajax-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup: function(m) {
+ return m;
+ },
});
- },
- initSelection: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.initSelection.apply(_this, args);
- },
- formatResult: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
- },
- formatSelection: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup: function(m) {
- return m;
- },
- });
- };
- })(this),
- );
+ };
+ })(this),
+ );
+ })
+ .catch(() => {});
}
UsersSelect.prototype.initSelection = function(element, callback) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
deleted file mode 100644
index 8780aa4bd1c..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MRWidgetOptions from './mr_widget_options.vue';
-
-export default MRWidgetOptions;
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 60cebbfc2b2..0cedbdbdfef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import MrWidgetOptions from './ee_switch_mr_widget_options';
+import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 5a9d86594b1..0ce9d271845 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -3,6 +3,9 @@ import _ from 'underscore';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
+import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
+import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
@@ -28,10 +31,7 @@ import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
import CheckingState from './components/states/mr_widget_checking.vue';
-import MRWidgetStore from './stores/ee_switch_mr_widget_store';
-import MRWidgetService from './services/ee_switch_mr_widget_service';
import eventHub from './event_hub';
-import stateMaps from './stores/ee_switch_state_maps';
import notify from '~/lib/utils/notify';
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
deleted file mode 100644
index ea2aabb78fe..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MRWidgetService from './mr_widget_service';
-
-export default MRWidgetService;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
deleted file mode 100644
index ebef30e3eab..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import getStateKey from './get_state_key';
-
-export default getStateKey;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
deleted file mode 100644
index 92a07c53f2d..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MergeRequestStore from './mr_widget_store';
-
-export default MergeRequestStore;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
deleted file mode 100644
index 50cf9503ea7..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import stateMaps from './state_maps';
-
-export default stateMaps;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index e5a52c6a7f6..ab194e84ab4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,5 +1,5 @@
import Timeago from 'timeago.js';
-import getStateKey from './ee_switch_get_state_key';
+import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 95f4395ac13..a6c1737dcab 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -68,7 +68,8 @@ export default {
sanitizedSource() {
let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
// Only adds the width to the URL if its not a base64 data image
- if (!baseSrc.startsWith('data:') && !baseSrc.includes('?')) baseSrc += `?width=${this.size}`;
+ if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
+ baseSrc += `?width=${this.size}`;
return baseSrc;
},
resultantSrcAttribute() {
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index d24fe1b547e..f9773622001 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -28,10 +28,10 @@ export default {
},
computed: {
statusHtml() {
- if (this.user.status.emoji && this.user.status.message) {
- return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message}`;
- } else if (this.user.status.message) {
- return this.user.status.message;
+ if (this.user.status.emoji && this.user.status.message_html) {
+ return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
+ } else if (this.user.status.message_html) {
+ return this.user.status.message_html;
}
return '';
},
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 553cc44fe83..1f24b8dfa9e 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -395,6 +395,11 @@ $ide-commit-header-height: 48px;
svg {
vertical-align: sub;
}
+
+ .ide-status-avatar {
+ float: none;
+ margin: 0 0 1px;
+ }
}
.ide-status-file {
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index c5b9d1f6885..811cc310a8f 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -167,12 +167,14 @@
font-weight: $gl-font-weight-normal;
display: inline-block;
color: $gl-text-color;
+ vertical-align: top;
}
.option-description,
.option-disabled-reason {
margin-left: 30px;
color: $project-option-descr-color;
+ margin-top: -5px;
}
.option-disabled-reason {
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index ca713192c9e..6402e01ddc0 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -35,7 +35,9 @@ module MembershipActions
respond_to do |format|
format.html do
- message = "User was successfully removed from #{source_type}."
+ source = source_type == 'group' ? 'group and any subresources' : source_type
+
+ message = "User was successfully removed from #{source}."
redirect_to members_page_url, notice: message
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 1b30b4dda36..2b1395f364f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -8,7 +8,7 @@ class Import::BitbucketController < Import::BaseController
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
- response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
+ response = client.auth_code.get_token(params[:code], redirect_uri: users_import_bitbucket_callback_url)
session[:bitbucket_token] = response.token
session[:bitbucket_expires_at] = response.expires_at
@@ -89,7 +89,7 @@ class Import::BitbucketController < Import::BaseController
end
def go_to_bitbucket_for_permissions
- redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
+ redirect_to client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
end
def bitbucket_unauthorized
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 34c7dbdc2fe..3fbc0817e95 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -83,7 +83,7 @@ class Import::GithubController < Import::BaseController
end
def callback_import_url
- public_send("callback_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
+ public_send("users_import_#{provider}_callback_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def provider_unauthorized
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index a63eea0ca0e..1a1b024d766 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -15,6 +15,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:area_chart, project)
end
+ # Returns all environments or all folders based on the :nested param
def index
@environments = project.environments
.with_state(params[:scope] || :available)
@@ -25,11 +26,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: {
- environments: EnvironmentSerializer
- .new(project: @project, current_user: @current_user)
- .with_pagination(request, response)
- .within_folders
- .represent(@environments),
+ environments: serialize_environments(request, response, params[:nested]),
available_count: project.environments.available.count,
stopped_count: project.environments.stopped.count
}
@@ -37,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ # Returns all environments for a given folder
# rubocop: disable CodeReuse/ActiveRecord
def folder
folder_environments = project.environments.where(environment_type: params[:id])
@@ -48,10 +46,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
format.html
format.json do
render json: {
- environments: EnvironmentSerializer
- .new(project: @project, current_user: @current_user)
- .with_pagination(request, response)
- .represent(@environments),
+ environments: serialize_environments(request, response),
available_count: folder_environments.available.count,
stopped_count: folder_environments.stopped.count
}
@@ -186,6 +181,14 @@ class Projects::EnvironmentsController < Projects::ApplicationController
@environment ||= project.environments.find(params[:id])
end
+ def serialize_environments(request, response, nested = false)
+ serializer = EnvironmentSerializer
+ .new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
+ serializer = serializer.within_folders if nested
+ serializer.represent(@environments)
+ end
+
def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index fd5f3eeaa99..69f983f7ccd 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -19,7 +19,7 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
- prepend_before_action :authenticate_new_issue!, only: [:new]
+ prepend_before_action :authenticate_user!, only: [:new]
prepend_before_action :store_uri, only: [:new, :show]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
@@ -249,14 +249,6 @@ class Projects::IssuesController < Projects::ApplicationController
] + [{ label_ids: [], assignee_ids: [] }]
end
- def authenticate_new_issue!
- return if current_user
-
- notice = "Please sign in to create the new issue."
-
- redirect_to new_user_session_path, notice: notice
- end
-
def store_uri
if request.get? && !request.xhr?
store_location_for :user, request.fullpath
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index babeee48ef3..013e01b82aa 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -5,7 +5,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
include WorkhorseRequest
include SendFileUpload
- skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
+ skip_before_action :verify_workhorse_api!, only: :download
def download
lfs_object = LfsObject.find_by_oid(oid)
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 368ee89ff5c..54ff7ded8e5 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -39,8 +39,11 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
end
def set_pipeline_variables
- @pipelines = @merge_request.all_pipelines
- @pipeline = @merge_request.head_pipeline
- @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0
+ @pipelines =
+ if can?(current_user, :read_pipeline, @project)
+ @merge_request.all_pipelines
+ else
+ Ci::Pipeline.none
+ end
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 67827b1d3bb..6a86f8ca729 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -4,6 +4,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :authorize_read_pipeline!
+ before_action :authorize_read_build!, only: [:index]
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
@@ -69,7 +70,7 @@ class Projects::PipelinesController < Projects::ApplicationController
render json: PipelineSerializer
.new(project: @project, current_user: @current_user)
- .represent(@pipeline, grouped: true)
+ .represent(@pipeline, show_represent_params)
end
end
end
@@ -157,6 +158,10 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def show_represent_params
+ { grouped: true }
+ end
+
def create_params
params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 75e590f3f33..f2f63e986bb 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -99,7 +99,9 @@ module Projects
def define_triggers_variables
@triggers = @project.triggers
+ .present(current_user: current_user)
@trigger = ::Ci::Trigger.new
+ .present(current_user: current_user)
end
def define_badges_variables
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index f5fdfb8accc..c7b4ebb2b24 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -66,12 +66,11 @@ class Projects::TriggersController < Projects::ApplicationController
end
def trigger
- @trigger ||= project.triggers.find(params[:id]) || render_404
+ @trigger ||= project.triggers.find(params[:id])
+ .present(current_user: current_user)
end
def trigger_params
- params.require(:trigger).permit(
- :description
- )
+ params.require(:trigger).permit(:description)
end
end
diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb
index c1ef9dfefa7..f8c7f0c3167 100644
--- a/app/finders/contributed_projects_finder.rb
+++ b/app/finders/contributed_projects_finder.rb
@@ -14,6 +14,9 @@ class ContributedProjectsFinder < UnionFinder
# Returns an ActiveRecord::Relation.
# rubocop: disable CodeReuse/ActiveRecord
def execute(current_user = nil)
+ # Do not show contributed projects if the user profile is private.
+ return Project.none unless can_read_profile?(current_user)
+
segments = all_projects(current_user)
find_union(segments, Project).includes(:namespace).order_id_desc
@@ -22,6 +25,10 @@ class ContributedProjectsFinder < UnionFinder
private
+ def can_read_profile?(current_user)
+ Ability.allowed?(current_user, :read_user_profile, @user)
+ end
+
def all_projects(current_user)
projects = []
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 01a0fb34484..2b1d6f49878 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -16,6 +16,13 @@ module AuthHelper
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
+ def qa_class_for_provider(provider)
+ {
+ saml: 'qa-saml-login-button',
+ github: 'qa-github-login-button'
+ }[provider.to_sym]
+ end
+
def auth_providers
Gitlab::Auth::OAuth::Provider.providers
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index fa5d3ae474a..dedc58f482b 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -36,6 +36,14 @@ module EmailsHelper
nil
end
+ def sanitize_name(name)
+ if name =~ URI::DEFAULT_PARSER.regexp[:URI_REF]
+ name.tr('.', '_')
+ else
+ name
+ end
+ end
+
def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24
diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb
deleted file mode 100644
index e36d63b2946..00000000000
--- a/app/helpers/external_wiki_helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module ExternalWikiHelper
- def get_project_wiki_path(project)
- external_wiki_service = project.external_wiki
- if external_wiki_service
- external_wiki_service.properties['external_wiki_url']
- else
- project_wiki_path(project, :home)
- end
- end
-end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index ab4a1ccc0d1..11d5591d509 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -18,12 +18,13 @@ module MembersHelper
"remove #{member.user.name} from"
end
- "#{text} #{action} the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
+ "#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
end
def remove_member_title(member)
action = member.request? ? 'Deny access request' : 'Remove user'
- "#{action} from #{member.real_source_type.humanize(capitalize: false)}"
+
+ "#{action} from #{source_text(member)}"
end
def leave_confirmation_message(member_source)
@@ -35,4 +36,14 @@ module MembersHelper
options = params.slice(:search, :sort).merge(options).permit!
"#{request.path}?#{options.to_param}"
end
+
+ private
+
+ def source_text(member)
+ type = member.real_source_type.humanize(capitalize: false)
+
+ return type if member.request? || member.invite? || type != 'group'
+
+ 'group and any subresources'
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index eceee054ede..85248a16f50 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -305,7 +305,8 @@ module ProjectsHelper
nav_tabs << :container_registry
end
- if project.builds_enabled? && can?(current_user, :read_pipeline, project)
+ # Pipelines feature is tied to presence of builds
+ if can?(current_user, :read_build, project)
nav_tabs << :pipelines
end
@@ -313,19 +314,24 @@ module ProjectsHelper
nav_tabs << :operations
end
- if project.external_issue_tracker
- nav_tabs << :external_issue_tracker
- end
-
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
end
end
+ nav_tabs << external_nav_tabs(project)
+
nav_tabs.flatten
end
+ def external_nav_tabs(project)
+ [].tap do |tabs|
+ tabs << :external_issue_tracker if project.external_issue_tracker
+ tabs << :external_wiki if project.has_external_wiki?
+ end
+ end
+
def tab_ability_map
{
environments: :read_environment,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 35cf4f8d277..84010e40ef4 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -4,6 +4,7 @@ module Ci
class Build < CommitStatus
prepend ArtifactMigratable
include Ci::Processable
+ include Ci::Metadatable
include TokenAuthenticatable
include AfterCommitQueue
include ObjectStorage::BackgroundMove
@@ -37,12 +38,10 @@ module Ci
has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
end
- has_one :metadata, class_name: 'Ci::BuildMetadata', autosave: true
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
accepts_nested_attributes_for :runner_session
- delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
@@ -133,7 +132,6 @@ module Ci
before_save :ensure_token
before_destroy { unscoped_project }
- before_create :ensure_metadata
after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
@@ -261,10 +259,6 @@ module Ci
end
end
- def ensure_metadata
- metadata || build_metadata(project: project)
- end
-
def detailed_status(current_user)
Gitlab::Ci::Status::Build::Factory
.new(self, current_user)
@@ -284,18 +278,6 @@ module Ci
self.name == 'pages'
end
- # degenerated build is one that cannot be run by Runner
- def degenerated?
- self.options.blank?
- end
-
- def degenerate!
- Build.transaction do
- self.update!(options: nil, yaml_variables: nil)
- self.metadata&.destroy
- end
- end
-
def archived?
return true if degenerated?
@@ -639,22 +621,6 @@ module Ci
super || project.try(:build_coverage_regex)
end
- def options
- read_metadata_attribute(:options, :config_options, {})
- end
-
- def yaml_variables
- read_metadata_attribute(:yaml_variables, :config_variables, [])
- end
-
- def options=(value)
- write_metadata_attribute(:options, :config_options, value)
- end
-
- def yaml_variables=(value)
- write_metadata_attribute(:yaml_variables, :config_variables, value)
- end
-
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables if user.blank?
@@ -956,20 +922,5 @@ module Ci
def project_destroyed?
project.pending_delete?
end
-
- def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)
- read_attribute(legacy_key) || metadata&.read_attribute(metadata_key) || default_value
- end
-
- def write_metadata_attribute(legacy_key, metadata_key, value)
- # save to metadata or this model depending on the state of feature flag
- if Feature.enabled?(:ci_build_metadata_config)
- ensure_metadata.write_attribute(metadata_key, value)
- write_attribute(legacy_key, nil)
- else
- write_attribute(legacy_key, value)
- metadata&.write_attribute(metadata_key, nil)
- end
- end
end
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 38390f49217..cd8eb774cf5 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -10,7 +10,7 @@ module Ci
self.table_name = 'ci_builds_metadata'
- belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
before_create :set_build_project
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 55db42162ca..637148c4ce4 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,6 +4,7 @@ module Ci
class Trigger < ActiveRecord::Base
extend Gitlab::Ci::Model
include IgnorableColumn
+ include Presentable
ignore_column :deleted_at
@@ -29,7 +30,7 @@ module Ci
end
def short_token
- token[0...4]
+ token[0...4] if token.present?
end
def legacy?
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 01f4c58daa1..982e13e2845 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -11,6 +11,7 @@ class Commit
include Mentionable
include Referable
include StaticModel
+ include Presentable
include ::Gitlab::Utils::StrongMemoize
attr_mentionable :safe_message, pipeline: :single_line
@@ -304,7 +305,9 @@ class Commit
end
def last_pipeline
- @last_pipeline ||= pipelines.last
+ strong_memoize(:last_pipeline) do
+ pipelines.last
+ end
end
def status(ref = nil)
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 73a27326f6c..002f3e17891 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -15,7 +15,7 @@ module CacheMarkdownField
# Increment this number every time the renderer changes its output
CACHE_REDCARPET_VERSION = 3
CACHE_COMMONMARK_VERSION_START = 10
- CACHE_COMMONMARK_VERSION = 13
+ CACHE_COMMONMARK_VERSION = 14
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
new file mode 100644
index 00000000000..9eed9492b37
--- /dev/null
+++ b/app/models/concerns/ci/metadatable.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Ci
+ ##
+ # This module implements methods that need to read and write
+ # metadata for CI/CD entities.
+ #
+ module Metadatable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :metadata, class_name: 'Ci::BuildMetadata',
+ foreign_key: :build_id,
+ inverse_of: :build,
+ autosave: true
+
+ delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+ before_create :ensure_metadata
+ end
+
+ def ensure_metadata
+ metadata || build_metadata(project: project)
+ end
+
+ def degenerated?
+ self.options.blank?
+ end
+
+ def degenerate!
+ self.class.transaction do
+ self.update!(options: nil, yaml_variables: nil)
+ self.metadata&.destroy
+ end
+ end
+
+ def options
+ read_metadata_attribute(:options, :config_options, {})
+ end
+
+ def yaml_variables
+ read_metadata_attribute(:yaml_variables, :config_variables, [])
+ end
+
+ def options=(value)
+ write_metadata_attribute(:options, :config_options, value)
+ end
+
+ def yaml_variables=(value)
+ write_metadata_attribute(:yaml_variables, :config_variables, value)
+ end
+
+ private
+
+ def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)
+ read_attribute(legacy_key) || metadata&.read_attribute(metadata_key) || default_value
+ end
+
+ def write_metadata_attribute(legacy_key, metadata_key, value)
+ # save to metadata or this model depending on the state of feature flag
+ if Feature.enabled?(:ci_build_metadata_config)
+ ensure_metadata.write_attribute(metadata_key, value)
+ write_attribute(legacy_key, nil)
+ else
+ write_attribute(legacy_key, value)
+ metadata&.write_attribute(metadata_key, nil)
+ end
+ end
+ end
+end
diff --git a/app/models/lfs_download_object.rb b/app/models/lfs_download_object.rb
new file mode 100644
index 00000000000..6383f95d546
--- /dev/null
+++ b/app/models/lfs_download_object.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class LfsDownloadObject
+ include ActiveModel::Validations
+
+ attr_accessor :oid, :size, :link
+ delegate :sanitized_url, :credentials, to: :sanitized_uri
+
+ validates :oid, format: { with: /\A\h{64}\z/ }
+ validates :size, numericality: { greater_than_or_equal_to: 0 }
+ validates :link, public_url: { protocols: %w(http https) }
+
+ def initialize(oid:, size:, link:)
+ @oid = oid
+ @size = size
+ @link = link
+ end
+
+ def sanitized_uri
+ @sanitized_uri ||= Gitlab::UrlSanitizer.new(link)
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index b0f049438eb..8e071a8ff21 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -78,12 +78,15 @@ class Member < ActiveRecord::Base
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated
+ scope :with_user, -> (user) { where(user: user) }
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
+ scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
+
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?, unless: :importing?
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index fc49ee7ac8c..2c9e1ba1d80 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -12,6 +12,8 @@ class GroupMember < Member
validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) }
+ scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) }
+
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 016c18ce6c8..5372c6084f4 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -12,6 +12,10 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
+ scope :in_namespaces, ->(groups) do
+ joins('INNER JOIN projects ON projects.id = members.source_id')
+ .where('projects.namespace_id in (?)', groups.select(:id))
+ end
class << self
# Add users to projects with passed access option
diff --git a/app/models/project.rb b/app/models/project.rb
index da77479fe1f..b385b89449d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -377,8 +377,10 @@ class Project < ActiveRecord::Base
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
- access_level_attribute = ProjectFeature.access_level_attribute(feature)
- with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] })
+ access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
+ enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))
+
+ with_project_feature.where(enabled_feature)
}
# Picks a feature where the level is exactly that given.
@@ -465,7 +467,8 @@ class Project < ActiveRecord::Base
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user)
- visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
+ visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
+ min_access_level = ProjectFeature.required_minimum_access_level(feature)
if user&.admin?
with_feature_enabled(feature)
@@ -473,10 +476,15 @@ class Project < ActiveRecord::Base
column = ProjectFeature.quoted_access_level_column(feature)
with_project_feature
- .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
- visible,
- ProjectFeature::PRIVATE,
- user.authorizations_for_projects)
+ .where(
+ "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
+ " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
+ {
+ private: Gitlab::VisibilityLevel::PRIVATE,
+ public_visible: ProjectFeature::ENABLED,
+ private_visible: ProjectFeature::PRIVATE,
+ authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
+ })
else
with_feature_access_level(feature, visible)
end
@@ -530,6 +538,7 @@ class Project < ActiveRecord::Base
def reference_pattern
%r{
+ (?<!#{Gitlab::PathRegex::PATH_START_CHAR})
((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
(?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
}x
@@ -569,6 +578,14 @@ class Project < ActiveRecord::Base
end
end
+ def all_pipelines
+ if builds_enabled?
+ super
+ else
+ super.external
+ end
+ end
+
# returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 39f2b8fe0de..f700090a493 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -23,11 +23,11 @@ class ProjectFeature < ActiveRecord::Base
PUBLIC = 30
FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
class << self
def access_level_attribute(feature)
- feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
- raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+ feature = ensure_feature!(feature)
"#{feature}_access_level".to_sym
end
@@ -38,6 +38,21 @@ class ProjectFeature < ActiveRecord::Base
"#{table}.#{attribute}"
end
+
+ def required_minimum_access_level(feature)
+ feature = ensure_feature!(feature)
+
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST)
+ end
+
+ private
+
+ def ensure_feature!(feature)
+ feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
+ raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+
+ feature
+ end
end
# Default scopes force us to unscope here since a service may need to check
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 33bc6a561f9..aeba2843e5d 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -74,6 +74,14 @@ class ProjectTeam
end
alias_method :users, :members
+ # `members` method uses project_authorizations table which
+ # is updated asynchronously, on project move it still contains
+ # old members who may not have access to the new location,
+ # so we filter out only members of project or project's group
+ def members_in_project_and_ancestors
+ members.where(id: member_user_ids)
+ end
+
def guests
@guests ||= fetch_members(Gitlab::Access::GUEST)
end
@@ -191,4 +199,8 @@ class ProjectTeam
def group
project.group
end
+
+ def member_user_ids
+ Member.on_project_and_ancestors(project).select(:user_id)
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index f8ac230852f..691abe3175f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -754,8 +754,12 @@ class User < ApplicationRecord
#
# Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
- def authorizations_for_projects
- project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+ def authorizations_for_projects(min_access_level: nil)
+ authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+
+ return authorizations unless min_access_level.present?
+
+ authorizations.where('project_authorizations.access_level >= ?', min_access_level)
end
# Returns the projects this user has reporter (or greater) access to, limited
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index e42d78f47c5..2c90b8a73cd 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -10,6 +10,15 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
+ condition(:external_pipeline, scope: :subject, score: 0) do
+ @subject.external?
+ end
+
+ # Disallow users without permissions from accessing internal pipelines
+ rule { ~can?(:read_build) & ~external_pipeline }.policy do
+ prevent :read_pipeline
+ end
+
rule { protected_ref }.prevent :update_pipeline
rule { can?(:public_access) & branch_allows_collaboration }.policy do
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index a0706eaa46c..dd8c5d49cf4 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -18,6 +18,7 @@ class IssuePolicy < IssuablePolicy
prevent :read_issue_iid
prevent :update_issue
prevent :admin_issue
+ prevent :create_note
end
rule { locked }.policy do
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index f22843b6463..8d23e3abed3 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -18,6 +18,7 @@ class NotePolicy < BasePolicy
prevent :read_note
prevent :admin_note
prevent :resolve_note
+ prevent :award_emoji
end
rule { is_author }.policy do
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 040b5a73415..2b5cca76c20 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -28,7 +28,10 @@ class PersonalSnippetPolicy < BasePolicy
rule { anonymous }.prevent :comment_personal_snippet
- rule { can?(:comment_personal_snippet) }.enable :award_emoji
+ rule { can?(:comment_personal_snippet) }.policy do
+ enable :create_note
+ enable :award_emoji
+ end
rule { full_private_access }.enable :read_personal_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 12f9f29dcc1..cadbc5ae009 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -108,6 +108,10 @@ class ProjectPolicy < BasePolicy
condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
condition(:can_have_multiple_clusters) { multiple_clusters_available? }
+ condition(:internal_builds_disabled) do
+ !@subject.builds_enabled?
+ end
+
features = %w[
merge_requests
issues
@@ -196,7 +200,6 @@ class ProjectPolicy < BasePolicy
enable :read_build
enable :read_container_image
enable :read_pipeline
- enable :read_pipeline_schedule
enable :read_environment
enable :read_deployment
enable :read_merge_request
@@ -235,6 +238,7 @@ class ProjectPolicy < BasePolicy
enable :update_build
enable :create_pipeline
enable :update_pipeline
+ enable :read_pipeline_schedule
enable :create_pipeline_schedule
enable :create_merge_request_from
enable :create_wiki
@@ -314,13 +318,12 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:project_snippet))
end
- rule { wiki_disabled & ~has_external_wiki }.policy do
+ rule { wiki_disabled }.policy do
prevent(*create_read_update_admin_destroy(:wiki))
prevent(:download_wiki_code)
end
rule { builds_disabled | repository_disabled }.policy do
- prevent(*create_update_admin_destroy(:pipeline))
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
@@ -328,11 +331,22 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:deployment))
end
+ # There's two separate cases when builds_disabled is true:
+ # 1. When internal CI is disabled - builds_disabled && internal_builds_disabled
+ # - We do not prevent the user from accessing Pipelines to allow him to access external CI
+ # 2. When the user is not allowed to access CI - builds_disabled && ~internal_builds_disabled
+ # - We prevent the user from accessing Pipelines
+ rule { (builds_disabled & ~internal_builds_disabled) | repository_disabled }.policy do
+ prevent(*create_read_update_admin_destroy(:pipeline))
+ prevent(*create_read_update_admin_destroy(:commit_status))
+ end
+
rule { repository_disabled }.policy do
prevent :push_code
prevent :download_code
prevent :fork_project
prevent :read_commit_status
+ prevent :read_pipeline
prevent(*create_read_update_admin_destroy(:release))
end
@@ -359,7 +373,6 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_note
enable :read_pipeline
- enable :read_pipeline_schedule
enable :read_commit_status
enable :read_container_image
enable :download_code
@@ -378,7 +391,6 @@ class ProjectPolicy < BasePolicy
rule { public_builds & can?(:guest_access) }.policy do
enable :read_pipeline
- enable :read_pipeline_schedule
end
# These rules are included to allow maintainers of projects to push to certain
@@ -393,7 +405,7 @@ class ProjectPolicy < BasePolicy
end.enable :read_issue_iid
rule do
- (can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
+ (~guest & can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
end.enable :read_merge_request_iid
rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 7dafa33bb99..e5e005cee6d 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -43,4 +43,6 @@ class ProjectSnippetPolicy < BasePolicy
enable :update_project_snippet
enable :admin_project_snippet
end
+
+ rule { ~can?(:read_project_snippet) }.prevent :create_note
end
diff --git a/app/presenters/ci/trigger_presenter.rb b/app/presenters/ci/trigger_presenter.rb
new file mode 100644
index 00000000000..605c8f328a4
--- /dev/null
+++ b/app/presenters/ci/trigger_presenter.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Ci
+ class TriggerPresenter < Gitlab::View::Presenter::Delegated
+ presents :trigger
+
+ def has_token_exposed?
+ can?(current_user, :admin_trigger, trigger)
+ end
+
+ def token
+ if has_token_exposed?
+ trigger.token
+ else
+ trigger.short_token
+ end
+ end
+ end
+end
diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb
new file mode 100644
index 00000000000..05adbe1d4f5
--- /dev/null
+++ b/app/presenters/commit_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class CommitPresenter < Gitlab::View::Presenter::Simple
+ presents :commit
+
+ def status_for(ref)
+ can?(current_user, :read_commit_status, commit.project) && commit.status(ref)
+ end
+
+ def any_pipelines?
+ can?(current_user, :read_pipeline, commit.project) && commit.pipelines.any?
+ end
+end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 44b6ca299ae..c59e73f824c 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -170,6 +170,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
end
+ def can_read_pipeline?
+ pipeline && can?(current_user, :read_pipeline, pipeline)
+ end
+
def mergeable_discussions_state
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 7b1a0be75ca..62b23a889c8 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -4,6 +4,7 @@ class ClusterApplicationEntity < Grape::Entity
expose :name
expose :status_name, as: :status
expose :status_reason
+ expose :version
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) }
diff --git a/app/serializers/error_tracking/project_entity.rb b/app/serializers/error_tracking/project_entity.rb
new file mode 100644
index 00000000000..405d87ca0d0
--- /dev/null
+++ b/app/serializers/error_tracking/project_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectEntity < Grape::Entity
+ expose(*Gitlab::ErrorTracking::Project::ACCESSORS)
+ end
+end
diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb
new file mode 100644
index 00000000000..b2406f4d631
--- /dev/null
+++ b/app/serializers/error_tracking/project_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectSerializer < BaseSerializer
+ entity ProjectEntity
+ end
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 9361c9f987b..f42abf06e1e 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -57,7 +57,7 @@ class MergeRequestWidgetEntity < IssuableEntity
end
expose :merge_commit_message
- expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
+ expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? }
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
# Booleans
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index ae0c644e6c0..f9717a9426b 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -2,9 +2,11 @@
module Members
class DestroyService < Members::BaseService
- def execute(member, skip_authorization: false)
+ def execute(member, skip_authorization: false, skip_subresources: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
+ @skip_auth = skip_authorization
+
return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy
@@ -15,6 +17,7 @@ module Members
notification_service.decline_access_request(member)
end
+ delete_subresources(member) unless skip_subresources
enqueue_delete_todos(member)
after_execute(member: member)
@@ -24,6 +27,29 @@ module Members
private
+ def delete_subresources(member)
+ return unless member.is_a?(GroupMember) && member.user && member.group
+
+ delete_project_members(member)
+ delete_subgroup_members(member) if Group.supports_nested_objects?
+ end
+
+ def delete_project_members(member)
+ groups = member.group.self_and_descendants
+
+ ProjectMember.in_namespaces(groups).with_user(member.user).each do |project_member|
+ self.class.new(current_user).execute(project_member, skip_authorization: @skip_auth)
+ end
+ end
+
+ def delete_subgroup_members(member)
+ groups = member.group.descendants
+
+ GroupMember.in_groups(groups).with_user(member.user).each do |group_member|
+ self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
+ end
+ end
+
def can_destroy_member?(member)
can?(current_user, destroy_member_permission(member), member)
end
diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb
index 7b92fe6fe14..bae98ede561 100644
--- a/app/services/notes/build_service.rb
+++ b/app/services/notes/build_service.rb
@@ -9,7 +9,7 @@ module Notes
if in_reply_to_discussion_id.present?
discussion = find_discussion(in_reply_to_discussion_id)
- unless discussion
+ unless discussion && can?(current_user, :create_note, discussion.noteable)
note = Note.new
note.errors.add(:base, 'Discussion to reply to cannot be found')
return note
@@ -34,19 +34,8 @@ module Notes
if project
project.notes.find_discussion(discussion_id)
else
- discussion = Note.find_discussion(discussion_id)
- noteable = discussion.noteable
-
- return nil unless noteable_without_project?(noteable)
-
- discussion
+ Note.find_discussion(discussion_id)
end
end
-
- def noteable_without_project?(noteable)
- return true if noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
-
- false
- end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e1cf327209b..1a65561dd70 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -373,7 +373,8 @@ class NotificationService
end
def project_was_moved(project, old_path_with_namespace)
- recipients = notifiable_users(project.team.members, :mention, project: project)
+ recipients = project.private? ? project.team.members_in_project_and_ancestors : project.team.members
+ recipients = notifiable_users(recipients, :mention, project: project)
recipients.each do |recipient|
mailer.project_was_moved_email(
diff --git a/app/services/projects/import_error_filter.rb b/app/services/projects/import_error_filter.rb
new file mode 100644
index 00000000000..a0fc5149bb4
--- /dev/null
+++ b/app/services/projects/import_error_filter.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Projects
+ # Used by project imports, it removes any potential paths
+ # included in an error message that could be stored in the DB
+ class ImportErrorFilter
+ ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/
+ FILTER_MESSAGE = '[FILTERED]'
+
+ def self.filter_message(message)
+ message.gsub(ERROR_MESSAGE_FILTER, FILTER_MESSAGE)
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 0c426faa22d..5861b803996 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -24,8 +24,16 @@ module Projects
import_data
success
- rescue => e
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type })
+
error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{e.message}")
+ rescue => e
+ message = Projects::ImportErrorFilter.filter_message(e.message)
+
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type })
+
+ error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{message}")
end
private
@@ -35,7 +43,7 @@ module Projects
begin
Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
- raise Error, "Blocked import URL: #{e.message}"
+ raise e, "Blocked import URL: #{e.message}"
end
end
@@ -86,11 +94,11 @@ module Projects
return unless project.lfs_enabled?
- oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
- download_service = Projects::LfsPointers::LfsDownloadService.new(project)
+ lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
- oids_to_download.each do |oid, link|
- download_service.execute(oid, link)
+ lfs_objects_to_download.each do |lfs_download_object|
+ Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object)
+ .execute
end
rescue => e
# Right now, to avoid aborting the importing process, we silently fail
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
index a837ea82e38..7998976b00a 100644
--- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -41,16 +41,17 @@ module Projects
end
def parse_response_links(objects_response)
- objects_response.each_with_object({}) do |entry, link_list|
+ objects_response.each_with_object([]) do |entry, link_list|
begin
- oid = entry['oid']
link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
raise DownloadLinkNotFound unless link
- link_list[oid] = add_credentials(link)
- rescue DownloadLinkNotFound, URI::InvalidURIError
- Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.")
+ link_list << LfsDownloadObject.new(oid: entry['oid'],
+ size: entry['size'],
+ link: add_credentials(link))
+ rescue DownloadLinkNotFound, Addressable::URI::InvalidURIError
+ log_error("Link for Lfs Object with oid #{entry['oid']} not found or invalid.")
end
end
end
@@ -70,7 +71,7 @@ module Projects
end
def add_credentials(link)
- uri = URI.parse(link)
+ uri = Addressable::URI.parse(link)
if should_add_credentials?(uri)
uri.user = remote_uri.user
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index b5128443435..398f00a598d 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -4,68 +4,93 @@
module Projects
module LfsPointers
class LfsDownloadService < BaseService
- VALID_PROTOCOLS = %w[http https].freeze
+ SizeError = Class.new(StandardError)
+ OidError = Class.new(StandardError)
- # rubocop: disable CodeReuse/ActiveRecord
- def execute(oid, url)
- return unless project&.lfs_enabled? && oid.present? && url.present?
+ attr_reader :lfs_download_object
+ delegate :oid, :size, :credentials, :sanitized_url, to: :lfs_download_object, prefix: :lfs
- return if LfsObject.exists?(oid: oid)
+ def initialize(project, lfs_download_object)
+ super(project)
- sanitized_uri = sanitize_url!(url)
+ @lfs_download_object = lfs_download_object
+ end
- with_tmp_file(oid) do |file|
- download_and_save_file(file, sanitized_uri)
- lfs_object = LfsObject.new(oid: oid, size: file.size, file: file)
+ # rubocop: disable CodeReuse/ActiveRecord
+ def execute
+ return unless project&.lfs_enabled? && lfs_download_object
+ return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
+ return if LfsObject.exists?(oid: lfs_oid)
- project.all_lfs_objects << lfs_object
+ wrap_download_errors do
+ download_lfs_file!
end
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
- Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded: #{e.message}")
- rescue StandardError => e
- Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
end
# rubocop: enable CodeReuse/ActiveRecord
private
- def sanitize_url!(url)
- Gitlab::UrlSanitizer.new(url).tap do |sanitized_uri|
- # Just validate that HTTP/HTTPS protocols are used. The
- # subsequent Gitlab::HTTP.get call will do network checks
- # based on the settings.
- Gitlab::UrlBlocker.validate!(sanitized_uri.sanitized_url,
- protocols: VALID_PROTOCOLS)
+ def wrap_download_errors(&block)
+ yield
+ rescue SizeError, OidError, StandardError => e
+ error("LFS file with oid #{lfs_oid} could't be downloaded from #{lfs_sanitized_url}: #{e.message}")
+ end
+
+ def download_lfs_file!
+ with_tmp_file do |tmp_file|
+ download_and_save_file!(tmp_file)
+ project.all_lfs_objects << LfsObject.new(oid: lfs_oid,
+ size: lfs_size,
+ file: tmp_file)
+
+ success
end
end
- def download_and_save_file(file, sanitized_uri)
- response = Gitlab::HTTP.get(sanitized_uri.sanitized_url, headers(sanitized_uri)) do |fragment|
+ def download_and_save_file!(file)
+ digester = Digest::SHA256.new
+ response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment|
+ digester << fragment
file.write(fragment)
+
+ raise_size_error! if file.size > lfs_size
end
raise StandardError, "Received error code #{response.code}" unless response.success?
- end
- def headers(sanitized_uri)
- query_options.tap do |headers|
- credentials = sanitized_uri.credentials
+ raise_size_error! if file.size != lfs_size
+ raise_oid_error! if digester.hexdigest != lfs_oid
+ end
- if credentials[:user].present? || credentials[:password].present?
+ def download_headers
+ { stream_body: true }.tap do |headers|
+ if lfs_credentials[:user].present? || lfs_credentials[:password].present?
# Using authentication headers in the request
- headers[:http_basic_authentication] = [credentials[:user], credentials[:password]]
+ headers[:basic_auth] = { username: lfs_credentials[:user], password: lfs_credentials[:password] }
end
end
end
- def query_options
- { stream_body: true }
- end
-
- def with_tmp_file(oid)
+ def with_tmp_file
create_tmp_storage_dir
- File.open(File.join(tmp_storage_dir, oid), 'wb') { |file| yield file }
+ File.open(tmp_filename, 'wb') do |file|
+ begin
+ yield file
+ rescue StandardError => e
+ # If the lfs file is successfully downloaded it will be removed
+ # when it is added to the project's lfs files.
+ # Nevertheless if any excetion raises the file would remain
+ # in the file system. Here we ensure to remove it
+ File.unlink(file) if File.exist?(file)
+
+ raise e
+ end
+ end
+ end
+
+ def tmp_filename
+ File.join(tmp_storage_dir, lfs_oid)
end
def create_tmp_storage_dir
@@ -79,6 +104,20 @@ module Projects
def storage_dir
@storage_dir ||= Gitlab.config.lfs.storage_path
end
+
+ def raise_size_error!
+ raise SizeError, 'Size mistmatch'
+ end
+
+ def raise_oid_error!
+ raise OidError, 'Oid mismatch'
+ end
+
+ def error(message, http_status = nil)
+ log_error(message)
+
+ super
+ end
end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index eb2478be3cf..5caeb4cfa5f 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -7,7 +7,11 @@ module Projects
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
- SITE_PATH = 'public/'.freeze
+ PUBLIC_DIR = 'public'.freeze
+
+ # this has to be invalid group name,
+ # as it shares the namespace with groups
+ TMP_EXTRACT_PATH = '@pages.tmp'.freeze
attr_reader :build
@@ -27,12 +31,11 @@ module Projects
raise InvalidStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
- FileUtils.mkdir_p(tmp_path)
- Dir.mktmpdir(nil, tmp_path) do |archive_path|
+ make_secure_tmp_dir(tmp_path) do |archive_path|
extract_archive!(archive_path)
# Check if we did extract public directory
- archive_public_path = File.join(archive_path, 'public')
+ archive_public_path = File.join(archive_path, PUBLIC_DIR)
raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
raise InvalidStateError, 'pages are outdated' unless latest?
@@ -85,22 +88,18 @@ module Projects
raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
- public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
+ public_entry = build.artifacts_metadata_entry(PUBLIC_DIR + '/', recursive: true)
if public_entry.total_size > max_size
raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
end
- # Requires UnZip at least 6.00 Info-ZIP.
- # -qq be (very) quiet
- # -n never overwrite existing files
- # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
- site_path = File.join(SITE_PATH, '*')
build.artifacts_file.use_file do |artifacts_path|
- unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path}))
- raise FailedToExtractError, 'pages failed to extract'
- end
+ SafeZip::Extract.new(artifacts_path)
+ .extract(directories: [PUBLIC_DIR], to: temp_path)
end
+ rescue SafeZip::Extract::Error => e
+ raise FailedToExtractError, e.message
end
def deploy_page!(archive_public_path)
@@ -139,7 +138,7 @@ module Projects
end
def tmp_path
- @tmp_path ||= File.join(::Settings.pages.path, 'tmp')
+ @tmp_path ||= File.join(::Settings.pages.path, TMP_EXTRACT_PATH)
end
def pages_path
@@ -147,11 +146,11 @@ module Projects
end
def public_path
- @public_path ||= File.join(pages_path, 'public')
+ @public_path ||= File.join(pages_path, PUBLIC_DIR)
end
def previous_public_path
- @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}")
+ @previous_public_path ||= File.join(pages_path, "#{PUBLIC_DIR}.#{SecureRandom.hex}")
end
def ref
@@ -188,5 +187,15 @@ module Projects
def pages_deployments_failed_total_counter
@pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
end
+
+ def make_secure_tmp_dir(tmp_path)
+ FileUtils.mkdir_p(tmp_path)
+ path = Dir.mktmpdir(nil, tmp_path)
+ begin
+ yield(path)
+ ensure
+ FileUtils.remove_entry_secure(path)
+ end
+ end
end
end
diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb
index 4340d3e8260..9b85e13107b 100644
--- a/app/services/protected_branches/api_service.rb
+++ b/app/services/protected_branches/api_service.rb
@@ -6,8 +6,6 @@ module ProtectedBranches
@push_params = AccessLevelParams.new(:push, params)
@merge_params = AccessLevelParams.new(:merge, params)
- verify_params!
-
protected_branch_params = {
name: params[:name],
push_access_levels_attributes: @push_params.access_levels,
@@ -16,11 +14,5 @@ module ProtectedBranches
::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
end
-
- private
-
- def verify_params!
- # EE-only
- end
end
end
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index b75454b33d7..ec57eb1ed08 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -18,12 +18,12 @@
.table-mobile-content
= link_to runner.short_sha, admin_runner_path(runner)
- .table-section.section-15
+ .table-section.section-20
.table-mobile-header{ role: 'rowheader' }= _('Description')
.table-mobile-content.str-truncated.has-tooltip{ title: runner.description }
= runner.description
- .table-section.section-15
+ .table-section.section-10
.table-mobile-header{ role: 'rowheader' }= _('Version')
.table-mobile-content.str-truncated.has-tooltip{ title: runner.version }
= runner.version
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index e9e4e0847d3..81380587fd2 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -106,8 +106,8 @@
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-10{ role: 'rowheader' }= _('Type')
.table-section.section-10{ role: 'rowheader' }= _('Runner token')
- .table-section.section-15{ role: 'rowheader' }= _('Description')
- .table-section.section-15{ role: 'rowheader' }= _('Version')
+ .table-section.section-20{ role: 'rowheader' }= _('Description')
+ .table-section.section-10{ role: 'rowheader' }= _('Version')
.table-section.section-10{ role: 'rowheader' }= _('IP Address')
.table-section.section-5{ role: 'rowheader' }= _('Projects')
.table-section.section-5{ role: 'rowheader' }= _('Jobs')
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
index 3d0a1f622a5..ccc3e734276 100644
--- a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
@@ -1,5 +1,5 @@
#content
- = email_default_heading("#{@resource.user.name}, you've added an additional email!")
+ = email_default_heading("#{sanitize_name(@resource.user.name)}, you've added an additional email!")
%p Click the link below to confirm your email address (#{@resource.email})
#cta
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 12271ee5adb..1b583ea85d6 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -5,7 +5,7 @@
.d-flex.justify-content-between.flex-wrap
- providers.each do |provider|
- has_icon = provider_has_icon?(provider)
- = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login qa-saml-login-button', id: "oauth-login-#{provider}" do
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn d-flex align-items-center omniauth-btn text-left oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do
- if has_icon
= provider_image_tag(provider)
%span
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index b3d13a2dc43..b0ba846f204 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -1,20 +1,20 @@
= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
+ = form_errors(@milestone)
.row
- = form_errors(@milestone)
-
.col-md-6
.form-group.row
- = f.label :title, "Title", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :title, "Title"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.row.milestone-description
- = f.label :description, "Description", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :description, "Description"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false
- .clearfix
- .error-alert
-
+ .clearfix
+ .error-alert
= render "shared/milestones/form_dates", f: f
.form-actions
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 207c08ee5bb..dd7833647b7 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -281,19 +281,34 @@
%strong.fly-out-top-item-name
= _('Registry')
- - if project_nav_tab? :wiki
+ - if project_nav_tab?(:wiki)
+ - wiki_url = project_wiki_path(@project, :home)
= nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki qa-wiki-link' do
+ = link_to wiki_url, class: 'shortcuts-wiki qa-wiki-link' do
.nav-icon-container
= sprite_icon('book')
%span.nav-item-name
= _('Wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
- = link_to get_project_wiki_path(@project) do
+ = link_to wiki_url do
%strong.fly-out-top-item-name
= _('Wiki')
+ - if project_nav_tab?(:external_wiki)
+ - external_wiki_url = @project.external_wiki.external_wiki_url
+ = nav_link do
+ = link_to external_wiki_url, class: 'shortcuts-external_wiki' do
+ .nav-icon-container
+ = sprite_icon('issue-external')
+ %span.nav-item-name
+ = _('External Wiki')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(html_options: { class: "fly-out-top-item" } ) do
+ = link_to external_wiki_url do
+ %strong.fly-out-top-item-name
+ = _('External Wiki')
+
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
index 50209c46ed1..5a67214059c 100644
--- a/app/views/notify/_note_email.text.erb
+++ b/app/views/notify/_note_email.text.erb
@@ -3,7 +3,7 @@
<% discussion = note.discussion if note.part_of_discussion? -%>
<% if discussion && !discussion.individual_note? -%>
-<%= note.author_name -%>
+<%= sanitize_name(note.author_name) -%>
<% if discussion.new_discussion? -%>
<%= " started a new discussion" -%>
<% else -%>
@@ -16,7 +16,7 @@
<% elsif Gitlab::CurrentSettings.email_author_in_body -%>
-<%= "#{note.author_name} commented:" -%>
+<%= "#{sanitize_name(note.author_name)} commented:" -%>
<% end -%>
diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb
index 695780c3145..bf863952478 100644
--- a/app/views/notify/autodevops_disabled_email.text.erb
+++ b/app/views/notify/autodevops_disabled_email.text.erb
@@ -3,7 +3,7 @@ Auto DevOps pipeline was disabled for <%= @project.name %>
The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>).
<% if @pipeline.user -%>
- Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index b7284dd819b..eb148d72da1 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was closed by #{@updated_by.name}
+ Issue was closed by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index b35d4b7502d..b1f0a3f37ec 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-Issue was closed by #{@updated_by.name}
+Issue was closed by #{sanitize_name(@updated_by.name)}
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml
index 44e018304e1..2aa753e0d55 100644
--- a/app/views/notify/closed_merge_request_email.html.haml
+++ b/app/views/notify/closed_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
+ Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml
index c4e06cb3cb1..1094d584a1c 100644
--- a/app/views/notify/closed_merge_request_email.text.haml
+++ b/app/views/notify/closed_merge_request_email.text.haml
@@ -1,8 +1,8 @@
-Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
+Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)}
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml
index b6051b11cea..66e73a9b03f 100644
--- a/app/views/notify/issue_status_changed_email.html.haml
+++ b/app/views/notify/issue_status_changed_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was #{@issue_status} by #{@updated_by.name}
+ Issue was #{@issue_status} by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/issue_status_changed_email.text.erb b/app/views/notify/issue_status_changed_email.text.erb
index 4200881f7e8..f38b09e9820 100644
--- a/app/views/notify/issue_status_changed_email.text.erb
+++ b/app/views/notify/issue_status_changed_email.text.erb
@@ -1,4 +1,4 @@
-Issue was <%= @issue_status %> by <%= @updated_by.name %>
+Issue was <%= @issue_status %> by <%= sanitize_name(@updated_by.name) %>
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
diff --git a/app/views/notify/member_access_requested_email.text.erb b/app/views/notify/member_access_requested_email.text.erb
index 9c5ee0eaf26..ddb4a7b3d2c 100644
--- a/app/views/notify/member_access_requested_email.text.erb
+++ b/app/views/notify/member_access_requested_email.text.erb
@@ -1,3 +1,3 @@
-<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+<%= sanitize_name(member.user.name) %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= polymorphic_url([member_source, :members]) %>
diff --git a/app/views/notify/member_invite_accepted_email.text.erb b/app/views/notify/member_invite_accepted_email.text.erb
index cef87101427..c824533eac2 100644
--- a/app/views/notify/member_invite_accepted_email.text.erb
+++ b/app/views/notify/member_invite_accepted_email.text.erb
@@ -1,3 +1,3 @@
-<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+<%= member.invite_email %>, now known as <%= sanitize_name(member.user.name) %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= member_source.web_url %>
diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb
index 0a6393355be..d944c3b4a50 100644
--- a/app/views/notify/member_invited_email.text.erb
+++ b/app/views/notify/member_invited_email.text.erb
@@ -1,4 +1,4 @@
-You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
+You have been invited <%= "by #{sanitize_name(member.created_by.name)} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
Accept invitation: <%= invite_url(@token) %>
Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml
index b487e26b122..ffb416abf72 100644
--- a/app/views/notify/merge_request_status_email.html.haml
+++ b/app/views/notify/merge_request_status_email.html.haml
@@ -1,2 +1,2 @@
%p
- Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
+ Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index ae2a2933865..b9b9e0c3ad7 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -1,8 +1,8 @@
-Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
+Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)}
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
index dcdd6db69d6..0c7bf1bb044 100644
--- a/app/views/notify/merge_request_unmergeable_email.text.haml
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 661c23bcbe2..045a43cbc84 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/new_gpg_key_email.html.haml b/app/views/notify/new_gpg_key_email.html.haml
index 4b9350c4e88..b857705e01f 100644
--- a/app/views/notify/new_gpg_key_email.html.haml
+++ b/app/views/notify/new_gpg_key_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user.name}!
+ Hi #{sanitize_name(@user.name)}!
%p
A new GPG key was added to your account:
%p
diff --git a/app/views/notify/new_gpg_key_email.text.erb b/app/views/notify/new_gpg_key_email.text.erb
index 80b5a1fd7ff..92ea851eee4 100644
--- a/app/views/notify/new_gpg_key_email.text.erb
+++ b/app/views/notify/new_gpg_key_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
A new GPG key was added to your account:
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index 3c716f77296..58a2bcbe5eb 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -1,7 +1,7 @@
New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
-Author: <%= @issue.author_name %>
+Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= @issue.assignee_list %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
index 23213106c5b..173091e4a80 100644
--- a/app/views/notify/new_mention_in_issue_email.text.erb
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -1,7 +1,7 @@
You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
-Author: <%= @issue.author_name %>
-Assignee: <%= @issue.assignee_list %>
+Author: <%= sanitize_name(@issue.author_name) %>
+Assignee: <%= sanitize_name(@issue.assignee_list) %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
index 6fcebb22fc4..96a4f3f9eac 100644
--- a/app/views/notify/new_mention_in_merge_request_email.text.erb
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -3,7 +3,7 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %>
<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
<%= merge_path_description(@merge_request, 'to') %>
-Author: <%= @merge_request.author_name %>
-Assignee: <%= @merge_request.assignee_name %>
+Author: <%= sanitize_name(@merge_request.author_name) %>
+Assignee: <%= sanitize_name(@merge_request.assignee_name) %>
<%= @merge_request.description %>
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 5acd45b74a7..db23447dd39 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -7,7 +7,7 @@
- if @merge_request.assignee_id.present?
%p
- Assignee: #{@merge_request.assignee_name}
+ Assignee: #{sanitize_name(@merge_request.assignee_name)}
= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
diff --git a/app/views/notify/new_ssh_key_email.html.haml b/app/views/notify/new_ssh_key_email.html.haml
index 63b0cbbd205..d031842be95 100644
--- a/app/views/notify/new_ssh_key_email.html.haml
+++ b/app/views/notify/new_ssh_key_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user.name}!
+ Hi #{sanitize_name(@user.name)}!
%p
A new public key was added to your account:
%p
diff --git a/app/views/notify/new_ssh_key_email.text.erb b/app/views/notify/new_ssh_key_email.text.erb
index 05b551c89a0..690357d69ed 100644
--- a/app/views/notify/new_ssh_key_email.text.erb
+++ b/app/views/notify/new_ssh_key_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
A new public key was added to your account:
diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml
index db4424a01f9..dfbb5c75bd3 100644
--- a/app/views/notify/new_user_email.html.haml
+++ b/app/views/notify/new_user_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user['name']}!
+ Hi #{sanitize_name(@user['name'])}!
%p
- if Gitlab::CurrentSettings.allow_signup?
Your account has been created successfully.
diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb
index dd9b71e3b84..f3f20f3bfba 100644
--- a/app/views/notify/new_user_email.text.erb
+++ b/app/views/notify/new_user_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
The Administrator created an account for you. Now you are a member of the company GitLab application.
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index 294238eee51..722eedf90be 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -10,20 +10,20 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%>
<% if commit.author -%>
-Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
-Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
+Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
<% end -%>
<% if @pipeline.user -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index 39622cf7f02..9aadf380f79 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -10,13 +10,13 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%>
<% if commit.author -%>
-Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
-Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
+Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
@@ -25,7 +25,7 @@ Committed by: <%= commit.committer_name %>
<% job_count = @pipeline.total_size -%>
<% stage_count = @pipeline.stages_count -%>
<% if @pipeline.user -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml
index 67744ec1cee..97258833cfc 100644
--- a/app/views/notify/push_to_merge_request_email.html.haml
+++ b/app/views/notify/push_to_merge_request_email.html.haml
@@ -1,5 +1,5 @@
%h3
- = @updated_by_user.name
+ = sanitize_name(@updated_by_user.name)
pushed new commits to merge request
= link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml
index 95759d127e2..10c8e158846 100644
--- a/app/views/notify/push_to_merge_request_email.text.haml
+++ b/app/views/notify/push_to_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-#{@updated_by_user.name} pushed new commits to merge request #{@merge_request.to_reference}
+#{sanitize_name(@updated_by_user.name)} pushed new commits to merge request #{@merge_request.to_reference}
\
#{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))}
\
diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml
index ee2f40e1683..6d25488a7e2 100644
--- a/app/views/notify/reassigned_issue_email.html.haml
+++ b/app/views/notify/reassigned_issue_email.html.haml
@@ -2,7 +2,7 @@
Assignee changed
- if @previous_assignees.any?
from
- %strong= @previous_assignees.map(&:name).to_sentence
+ %strong= sanitize_name(@previous_assignees.map(&:name).to_sentence)
to
- if @issue.assignees.any?
%strong= @issue.assignee_list
diff --git a/app/views/notify/reassigned_issue_email.text.erb b/app/views/notify/reassigned_issue_email.text.erb
index 6c357f1074a..7bf2e8e6ce3 100644
--- a/app/views/notify/reassigned_issue_email.text.erb
+++ b/app/views/notify/reassigned_issue_email.text.erb
@@ -2,5 +2,5 @@ Reassigned Issue <%= @issue.iid %>
<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
-Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%>
+Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml
index 24c2b08810b..e4f19bc3200 100644
--- a/app/views/notify/reassigned_merge_request_email.html.haml
+++ b/app/views/notify/reassigned_merge_request_email.html.haml
@@ -2,9 +2,9 @@
Assignee changed
- if @previous_assignee
from
- %strong= @previous_assignee.name
+ %strong= sanitize_name(@previous_assignee.name)
to
- if @merge_request.assignee_id
- %strong= @merge_request.assignee_name
+ %strong= sanitize_name(@merge_request.assignee_name)
- else
%strong Unassigned
diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb
index 998a40fefde..96c770b5219 100644
--- a/app/views/notify/reassigned_merge_request_email.text.erb
+++ b/app/views/notify/reassigned_merge_request_email.text.erb
@@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %>
<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
-Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
- to <%= "#{@merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'}" %>
+Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%>
+ to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %>
diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml
index 522421b7cc3..502b8f21e35 100644
--- a/app/views/notify/resolved_all_discussions_email.html.haml
+++ b/app/views/notify/resolved_all_discussions_email.html.haml
@@ -1,2 +1,2 @@
%p
- All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{@resolved_by.name}
+ All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{sanitize_name(@resolved_by.name)}
diff --git a/app/views/notify/resolved_all_discussions_email.text.erb b/app/views/notify/resolved_all_discussions_email.text.erb
index 2881f3e699e..c4b36bfe1a8 100644
--- a/app/views/notify/resolved_all_discussions_email.text.erb
+++ b/app/views/notify/resolved_all_discussions_email.text.erb
@@ -1,3 +1,3 @@
-All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= @resolved_by.name %>
+All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= sanitize_name(@resolved_by.name) %>
<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
index d8492abc638..c2329a7aa66 100644
--- a/app/views/projects/blob/viewers/_readme.html.haml
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -1,4 +1,4 @@
= icon('info-circle fw')
= succeed '.' do
To learn more about this project, read
- = link_to "the wiki", get_project_wiki_path(viewer.project)
+ = link_to "the wiki", project_wiki_path(viewer.project, :home)
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index f6666921a25..8b6e3e42ea1 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,9 +1,11 @@
+- any_pipelines = @commit.present(current_user: current_user).any_pipelines?
+
%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs
= nav_link(path: 'commit#show') do
= link_to project_commit_path(@project, @commit.id) do
Changes
%span.badge.badge-pill= @diffs.size
- - if can?(current_user, :read_pipeline, @project)
+ - if any_pipelines
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_project_commit_path(@project, @commit.id) do
Pipelines
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index a389261136a..90fee2d70be 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -74,8 +74,8 @@
%span.commit-info.merge-requests{ 'data-project-commit-path' => merge_requests_project_commit_path(@project, @commit.id, format: :json) }
= icon('spinner spin')
- - if @commit.last_pipeline
- - last_pipeline = @commit.last_pipeline
+ - last_pipeline = @commit.last_pipeline
+ - if can?(current_user, :read_pipeline, last_pipeline)
.well-segment.pipeline-info
.status-icon-container
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 79e32949db9..06f0cd9675e 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -9,10 +9,7 @@
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
- - if @commit.status
- = render "ci_menu"
- - else
- .block-connector
+ = render "ci_menu"
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true
.limited-width-notes
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 1a74b120c26..0d3c6e7027c 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -6,6 +6,7 @@
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
+- commit_status = commit.present(current_user: current_user).status_for(ref)
- link = commit_path(project, commit, merge_request: merge_request)
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
@@ -22,7 +23,7 @@
%span.commit-row-message.d-block.d-sm-none
&middot;
= commit.short_id
- - if commit.status(ref)
+ - if commit_status
.d-block.d-sm-none
= render_commit_status(commit, ref: ref)
- if commit.description?
@@ -45,7 +46,7 @@
- else
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
- - if commit.status(ref)
+ - if commit_status
= render_commit_status(commit, ref: ref)
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index c73d167303f..310e339ac8d 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -12,6 +12,7 @@
%ul.content-list.related-items-list
- has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- @merge_requests.each do |merge_request|
+ - merge_request = merge_request.present(current_user: current_user)
%li.list-item.py-0.px-0
.item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3
.item-contents
@@ -25,7 +26,7 @@
= merge_request.target_project.full_path
= merge_request.to_reference
%span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2
- - if merge_request.head_pipeline
+ - if merge_request.can_read_pipeline?
= render_pipeline_status(merge_request.head_pipeline, tooltip_placement: 'bottom')
- elsif has_any_head_pipeline
= icon('blank fw')
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 1df38db9fd4..ffdd96870ef 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -6,7 +6,7 @@
%li
- target = @project.repository.find_branch(branch).dereferenced_target
- pipeline = @project.pipeline_for(branch, target.sha) if target
- - if pipeline
+ - if can?(current_user, :read_pipeline, pipeline)
%span.related-branch-ci-status
= render_pipeline_status(pipeline)
%span.related-branch-info
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 02d2dbf0d61..ac29cd8f679 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -46,7 +46,7 @@
%li.issuable-status.d-none.d-sm-inline-block
= icon('ban')
CLOSED
- - if merge_request.head_pipeline
+ - if can?(current_user, :read_pipeline, merge_request.head_pipeline)
%li.issuable-pipeline-status.d-none.d-sm-inline-block
= render_pipeline_status(merge_request.head_pipeline)
- if merge_request.open? && merge_request.broken?
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 4779b5c434e..19f5bba75c4 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -5,11 +5,13 @@
.row
.col-md-6
.form-group.row
- = f.label :title, _('Title'), class: 'col-form-label col-sm-2'
+ .col-form-label.col-sm-2
+ = f.label :title, _('Title')
.col-sm-10
= f.text_field :title, maxlength: 255, class: 'qa-milestone-title form-control', required: true, autofocus: true
.form-group.row.milestone-description
- = f.label :description, _('Description'), class: 'col-form-label col-sm-2'
+ .col-form-label.col-sm-2
+ = f.label :description, _('Description')
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do
= render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: _('Write milestone description...')
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 0f0114d513c..69a47faabed 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -6,23 +6,22 @@
= preserve(markdown(commit.description, pipeline: :single_line))
.info-well
- - if commit.status
- .well-segment.pipeline-info
- .icon-container
- = icon('clock-o')
- = pluralize @pipeline.total_size, "job"
- - if @pipeline.ref
- from
- - if @pipeline.ref_exists?
- = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
- - else
- %span.ref-name
- = @pipeline.ref
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+ .well-segment.pipeline-info
+ .icon-container
+ = icon('clock-o')
+ = pluralize @pipeline.total_size, "job"
+ - if @pipeline.ref
+ from
+ - if @pipeline.ref_exists?
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - else
+ %span.ref-name
+ = @pipeline.ref
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
.well-segment
.icon-container
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index bb328f5344c..bfb275b9ef5 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -110,6 +110,9 @@
%li
go test -cover (Go)
%code coverage: \d+.\d+% of statements
+ %li
+ nyc npm test (NodeJS) -
+ %code All files[^|]*\|[^|]*\s+([\d\.]+)
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 7e4618e1a88..6f6f1e5e0c5 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -1,6 +1,6 @@
%tr
%td
- - if can?(current_user, :admin_trigger, trigger)
+ - if trigger.has_token_exposed?
%span= trigger.token
= clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard")
- else
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 7d8826e540c..d1556dbd077 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -16,7 +16,7 @@
.form-group.row
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12
- = f.text_field :title, class: 'form-control', value: @page.title
+ = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title
- if @page.persisted?
%span.edit-wiki-page-slug-tip
= icon('lightbulb-o')
@@ -31,7 +31,7 @@
.col-sm-12= f.label :content, class: 'control-label-full-width'
.col-sm-12
= render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do
- = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here…")
+ = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea qa-wiki-content-textarea', placeholder: s_("WikiPage|Write your content or drag files here…")
= render 'shared/notes/hints'
.clearfix
@@ -47,14 +47,14 @@
.form-group.row
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
- .col-sm-12= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
+ .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: commit_message
.form-actions
- if @page && @page.persisted?
- = f.submit _("Save changes"), class: 'btn-success btn'
+ = f.submit _("Save changes"), class: 'btn-success btn qa-save-changes-button'
.float-right
= link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped'
- else
- = f.submit s_("Wiki|Create page"), class: 'btn-success btn'
+ = f.submit s_("Wiki|Create page"), class: 'btn-success btn qa-create-page-button'
.float-right
= link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel'
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index aeef64fd7eb..94267b6e0cf 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project)
+- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home)
- breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki")
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 4d5fd55364c..8b348bb4e4f 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title @page.human_title
- wiki_breadcrumb_dropdown_links(@page.slug)
- page_title @page.human_title, _("Wiki")
-- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project)
+- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home)
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
index df3308abe0d..73eedcc1dc9 100644
--- a/app/views/shared/empty_states/_wikis.html.haml
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -2,7 +2,7 @@
- if can?(current_user, :create_wiki, @project)
- create_path = project_wiki_path(@project, params[:id], { view: 'create' })
- - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success', title: s_('WikiEmpty|Create your first page')
+ - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success qa-create-first-page-link', title: s_('WikiEmpty|Create your first page')
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
%h4.text-left
diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml
index a5f100e3469..d44017299b8 100644
--- a/app/views/shared/empty_states/_wikis_layout.html.haml
+++ b/app/views/shared/empty_states/_wikis_layout.html.haml
@@ -1,6 +1,6 @@
.row.empty-state
.col-12
- .svg-content
+ .svg-content.qa-svg-content
= image_tag image_path
.col-12
.text-content.text-center
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 922805958a5..4de89d7c7a0 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -1,11 +1,13 @@
.col-md-6
.form-group.row
- = f.label :start_date, "Start Date", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :start_date, "Start Date"
.col-sm-10
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date", autocomplete: 'off'
%a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
.form-group.row
- = f.label :due_date, "Due Date", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :due_date, "Due Date"
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off'
%a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index fea7e17be3d..e1564d57426 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -84,7 +84,7 @@
title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_issues_count)
- - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
+ - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
%span.icon-wrapper.pipeline-status
= render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top')
.updated-note