summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js11
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/blob/viewer/index.js3
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue97
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue5
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js8
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue58
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js6
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js4
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js32
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js6
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js12
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue8
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js23
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js32
-rw-r--r--app/assets/javascripts/transfer_edit.js (renamed from app/assets/javascripts/project_edit.js)6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue7
-rw-r--r--app/assets/javascripts/vue_shared/plugins/global_toast.js8
-rw-r--r--app/assets/stylesheets/framework/typography.scss12
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/utilities.scss6
-rw-r--r--app/controllers/admin/applications_controller.rb4
-rw-r--r--app/controllers/concerns/issuable_collections.rb28
-rw-r--r--app/controllers/concerns/paginated_collection.rb19
-rw-r--r--app/controllers/concerns/sessionless_authentication.rb4
-rw-r--r--app/controllers/concerns/static_object_external_storage.rb24
-rw-r--r--app/controllers/dashboard/snippets_controller.rb17
-rw-r--r--app/controllers/dashboard/todos_controller.rb32
-rw-r--r--app/controllers/explore/snippets_controller.rb13
-rw-r--r--app/controllers/profiles_controller.rb9
-rw-r--r--app/controllers/projects/forks_controller.rb15
-rw-r--r--app/controllers/projects/repositories_controller.rb4
-rw-r--r--app/controllers/projects/snippets_controller.rb19
-rw-r--r--app/controllers/snippets_controller.rb10
-rw-r--r--app/controllers/users_controller.rb12
-rw-r--r--app/helpers/application_helper.rb19
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/releases_helper.rb38
-rw-r--r--app/models/application_setting.rb17
-rw-r--r--app/models/application_setting_implementation.rb4
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/merge_request.rb7
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/oauth_access_token.rb2
-rw-r--r--app/models/pages/lookup_path.rb38
-rw-r--r--app/models/pages/virtual_domain.rb28
-rw-r--r--app/models/pages_domain.rb4
-rw-r--r--app/models/project.rb14
-rw-r--r--app/models/project_feature.rb4
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/user.rb17
-rw-r--r--app/presenters/event_presenter.rb20
-rw-r--r--app/services/ci/compare_reports_base_service.rb2
-rw-r--r--app/services/ci/register_job_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb21
-rw-r--r--app/services/search/global_service.rb2
-rw-r--r--app/views/admin/application_settings/_repository_static_objects.html.haml18
-rw-r--r--app/views/admin/application_settings/repository.html.haml11
-rw-r--r--app/views/admin/applications/index.html.haml3
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml2
-rw-r--r--app/views/clusters/clusters/user/_header.html.haml2
-rw-r--r--app/views/events/_event.atom.builder2
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/_event_scope.html.haml5
-rw-r--r--app/views/events/event/_common.html.haml4
-rw-r--r--app/views/events/event/_created_project.html.haml2
-rw-r--r--app/views/graphiql/rails/editors/show.html.erb18
-rw-r--r--app/views/groups/settings/_advanced.html.haml2
-rw-r--r--app/views/profiles/keys/_key_table.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml20
-rw-r--r--app/views/projects/buttons/_download_links.html.haml3
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/projects/releases/index.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml6
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/shared/snippets/_list.html.haml7
-rw-r--r--app/views/shared/snippets/_snippet.html.haml6
-rw-r--r--app/views/users/calendar_activities.html.haml2
93 files changed, 738 insertions, 300 deletions
diff --git a/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js b/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js
new file mode 100644
index 00000000000..6a40f1cbc5e
--- /dev/null
+++ b/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js
@@ -0,0 +1,11 @@
+export default {
+ data() {
+ return {
+ isCustomStageForm: false,
+ };
+ },
+ methods: {
+ showAddStageForm: () => {},
+ hideAddStageForm: () => {},
+ },
+};
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1d97ad5ec11..992c5e5e330 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -36,6 +36,7 @@ const Api = {
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases',
+ mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: 'api/:version/application/statistics',
group(groupId, callback) {
@@ -371,6 +372,14 @@ const Api = {
});
},
+ postMergeRequestPipeline(id, { mergeRequestId }) {
+ const url = Api.buildUrl(this.mergeRequestsPipeline)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':merge_request_iid', mergeRequestId);
+
+ return axios.post(url);
+ },
+
releases(id) {
const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id));
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index d246a1f6064..9ea455069f3 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
@@ -105,7 +106,6 @@ export default class BlobViewer {
toggleCopyButtonState() {
if (!this.copySourceBtn) return;
-
if (this.simpleViewer.getAttribute('data-loaded')) {
this.copySourceBtn.setAttribute('title', __('Copy source to clipboard'));
this.copySourceBtn.classList.remove('disabled');
@@ -152,7 +152,6 @@ export default class BlobViewer {
this.activeViewer = newViewer;
this.toggleCopyButtonState();
-
BlobViewer.loadViewer(newViewer)
.then(viewer => {
$(viewer).renderGFM();
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 4890f99e9d1..e5b030d4900 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -1,14 +1,19 @@
<script>
-import PipelinesService from '../../pipelines/services/pipelines_service';
-import PipelineStore from '../../pipelines/stores/pipelines_store';
-import pipelinesMixin from '../../pipelines/mixins/pipelines';
-import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
-import { getParameterByName } from '../../lib/utils/common_utils';
-import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import PipelinesService from '~/pipelines/services/pipelines_service';
+import PipelineStore from '~/pipelines/stores/pipelines_store';
+import pipelinesMixin from '~/pipelines/mixins/pipelines';
+import eventHub from '~/pipelines/event_hub';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
+import bp from '~/breakpoints';
export default {
components: {
TablePagination,
+ GlButton,
+ GlLoadingIcon,
},
mixins: [pipelinesMixin, CIPaginationMixin],
props: {
@@ -33,6 +38,21 @@ export default {
required: false,
default: 'child',
},
+ canRunPipeline: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ mergeRequestId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
@@ -53,6 +73,41 @@ export default {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
+ /**
+ * The Run Pipeline button can only be rendered when:
+ * - In MR view - we use `canRunPipeline` for that purpose
+ * - If the latest pipeline has the `detached_merge_request_pipeline` flag
+ *
+ * @returns {Boolean}
+ */
+ canRenderPipelineButton() {
+ return this.canRunPipeline && this.latestPipelineDetachedFlag;
+ },
+ /**
+ * Checks if either `detached_merge_request_pipeline` or
+ * `merge_request_pipeline` are tru in the first
+ * object in the pipelines array.
+ *
+ * @returns {Boolean}
+ */
+ latestPipelineDetachedFlag() {
+ const latest = this.state.pipelines[0];
+ return (
+ latest &&
+ latest.flags &&
+ (latest.flags.detached_merge_request_pipeline || latest.flags.merge_request_pipeline)
+ );
+ },
+ /**
+ * When we are on Desktop and the button is visible
+ * we need to add a negative margin to the table
+ * to make it inline with the button
+ *
+ * @returns {Boolean}
+ */
+ shouldAddNegativeMargin() {
+ return this.canRenderPipelineButton && bp.isDesktop();
+ },
},
created() {
this.service = new PipelinesService(this.endpoint);
@@ -77,6 +132,22 @@ export default {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
},
+ /**
+ * When the user clicks on the Run Pipeline button
+ * we need to make a post request and
+ * to update the table content once the request is finished.
+ *
+ * We are emitting an event through the eventHub using the old pattern
+ * to make use of the code in mixins/pipelines.js that handles all the
+ * table events
+ *
+ */
+ onClickRunPipeline() {
+ eventHub.$emit('runMergeRequestPipeline', {
+ projectId: this.projectId,
+ mergeRequestId: this.mergeRequestId,
+ });
+ },
},
};
</script>
@@ -99,11 +170,25 @@ export default {
/>
<div v-else-if="shouldRenderTable" class="table-holder">
+ <div v-if="canRenderPipelineButton" class="nav justify-content-end">
+ <gl-button
+ v-if="canRenderPipelineButton"
+ variant="success"
+ class="js-run-mr-pipeline prepend-top-10 btn-wide-on-xs"
+ :disabled="state.isRunningMergeRequestPipeline"
+ @click="onClickRunPipeline"
+ >
+ <gl-loading-icon v-if="state.isRunningMergeRequestPipeline" inline />
+ {{ s__('Pipelines|Run Pipeline') }}
+ </gl-button>
+ </div>
+
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
+ :class="{ 'negative-margin-top': shouldAddNegativeMargin }"
/>
</div>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
index d946594a069..63549596fac 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
@@ -23,7 +23,10 @@ export default {
</script>
<template>
- <div :class="{ active: isActive }" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded">
+ <div
+ :class="{ active: isActive }"
+ class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
+ >
<slot></slot>
<div v-if="canEdit" class="dropdown">
<gl-button
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index b3ae47af750..c9a6b10b2f3 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import Cookies from 'js-cookie';
import { GlEmptyState } from '@gitlab/ui';
import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins';
+import addStageMixin from 'ee_else_ce/analytics/cycle_analytics/mixins/add_stage_mixin';
import Flash from '../flash';
import { __ } from '~/locale';
import Translate from '../vue_shared/translate';
@@ -43,8 +44,12 @@ export default () => {
DateRangeDropdown: () =>
import('ee_component/analytics/shared/components/date_range_dropdown.vue'),
'stage-nav-item': stageNavItem,
+ CustomStageForm: () =>
+ import('ee_component/analytics/cycle_analytics/components/custom_stage_form.vue'),
+ AddStageButton: () =>
+ import('ee_component/analytics/cycle_analytics/components/add_stage_button.vue'),
},
- mixins: [filterMixins],
+ mixins: [filterMixins, addStageMixin],
data() {
return {
store: CycleAnalyticsStore,
@@ -124,6 +129,7 @@ export default () => {
return;
}
+ this.hideAddStageForm();
this.isLoadingStage = true;
this.store.setStageEvents([], stage);
this.store.setActiveStage(stage);
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 69ec6ab8600..bfcc726a030 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -57,26 +57,12 @@ export default {
required: true,
},
},
- data() {
- return {
- blobForkSuggestion: null,
- };
- },
computed: {
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
- hasExpandedDiscussions() {
- return this.diffHasExpandedDiscussions(this.diffFile);
- },
diffContentIDSelector() {
return `#diff-content-${this.diffFile.file_hash}`;
},
- icon() {
- if (this.diffFile.submodule) {
- return 'archive';
- }
- return this.diffFile.blob.icon;
- },
titleLink() {
if (this.diffFile.submodule) {
return this.diffFile.submodule_tree_url || this.diffFile.submodule_link;
@@ -99,9 +85,6 @@ export default {
return this.diffFile.file_path;
},
- titleTag() {
- return this.diffFile.file_hash ? 'a' : 'span';
- },
isUsingLfs() {
return this.diffFile.stored_externally && this.diffFile.external_storage === 'lfs';
},
@@ -135,9 +118,6 @@ export default {
isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
},
- showExpandDiffToFullFileEnabled() {
- return gon.features.expandDiffFullFile && !this.diffFile.is_fully_expanded;
- },
expandDiffToFullFileTitle() {
if (this.diffFile.isShowingFullFile) {
return s__('MRDiff|Show changes only');
@@ -156,21 +136,12 @@ export default {
'toggleFileDiscussionWrappers',
'toggleFullDiff',
]),
- handleToggleFile(e, checkTarget) {
- if (
- !checkTarget ||
- e.target === this.$refs.header ||
- (e.target.classList && e.target.classList.contains('diff-toggle-caret'))
- ) {
- this.$emit('toggleFile');
- }
+ handleToggleFile() {
+ this.$emit('toggleFile');
},
showForkMessage() {
this.$emit('showForkMessage');
},
- handleToggleDiscussions() {
- this.toggleFileDiscussionWrappers(this.diffFile);
- },
handleFileNameClick(e) {
const isLinkToOtherPage =
this.diffFile.submodule_tree_url || this.diffFile.submodule_link || this.discussionPath;
@@ -178,7 +149,6 @@ export default {
if (!isLinkToOtherPage) {
e.preventDefault();
const selector = this.diffContentIDSelector;
-
scrollToElement(document.querySelector(selector));
window.location.hash = selector;
}
@@ -191,22 +161,23 @@ export default {
<div
ref="header"
class="js-file-title file-title file-title-flex-parent"
- @click="handleToggleFile($event, true)"
+ @click.self="handleToggleFile"
>
<div class="file-header-content">
<icon
v-if="collapsible"
+ ref="collapseIcon"
:name="collapseIcon"
:size="16"
aria-hidden="true"
class="diff-toggle-caret append-right-5"
- @click.stop="handleToggle"
+ @click.stop="handleToggleFile"
/>
<a
v-once
id="diffFile.file_path"
ref="titleWrapper"
- class="append-right-4 js-title-wrapper"
+ class="append-right-4"
:href="titleLink"
@click="handleFileNameClick"
>
@@ -214,7 +185,7 @@ export default {
:file-name="filePath"
:size="18"
aria-hidden="true"
- css-classes="js-file-icon append-right-5"
+ css-classes="append-right-5"
/>
<span v-if="isFileRenamed">
<strong
@@ -260,12 +231,13 @@ export default {
<template v-if="diffFile.blob && diffFile.blob.readable_text">
<span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')">
<gl-button
+ ref="toggleDiscussionsButton"
:disabled="!diffHasDiscussions(diffFile)"
- :class="{ active: hasExpandedDiscussions }"
+ :class="{ active: diffHasExpandedDiscussions(diffFile) }"
class="js-btn-vue-toggle-comments btn"
data-qa-selector="toggle_comments_button"
type="button"
- @click="handleToggleDiscussions"
+ @click="toggleFileDiscussionWrappers(diffFile)"
>
<icon name="comment" />
</gl-button>
@@ -282,8 +254,9 @@ export default {
<a
v-if="diffFile.replaced_view_path"
+ ref="replacedFileButton"
:href="diffFile.replaced_view_path"
- class="btn view-file js-view-replaced-file"
+ class="btn view-file"
v-html="viewReplacedFileButtonText"
>
</a>
@@ -292,7 +265,7 @@ export default {
ref="expandDiffToFullFileButton"
v-gl-tooltip.hover
:title="expandDiffToFullFileTitle"
- class="expand-file js-expand-file"
+ class="expand-file"
@click="toggleFullDiff(diffFile.file_path)"
>
<gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline />
@@ -304,7 +277,7 @@ export default {
v-gl-tooltip.hover
:href="diffFile.view_path"
target="blank"
- class="view-file js-view-file-button"
+ class="view-file"
:title="viewFileButtonText"
>
<icon name="doc-text" />
@@ -312,12 +285,13 @@ export default {
<a
v-if="diffFile.external_url"
+ ref="externalLink"
v-gl-tooltip.hover
:href="diffFile.external_url"
:title="`View on ${diffFile.formatted_external_url}`"
target="_blank"
rel="noopener noreferrer"
- class="btn btn-file-option js-external-url"
+ class="btn btn-file-option"
>
<icon name="external-link" />
</a>
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
index ce0c9256148..cec824a529c 100644
--- a/app/assets/javascripts/groups/transfer_dropdown.js
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -14,7 +14,7 @@ export default class TransferDropdown {
}
buildDropdown() {
- const extraOptions = [{ id: '', text: __('No parent group') }, 'divider'];
+ const extraOptions = [{ id: '-1', text: __('No parent group') }, 'divider'];
this.groupDropdown.glDropdown({
selectable: true,
@@ -33,5 +33,6 @@ export default class TransferDropdown {
assignSelected(selected) {
this.parentInput.val(selected.id);
+ this.parentInput.change();
}
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index b6868e63716..52674107df2 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -333,7 +333,8 @@ export default class MergeRequestTabs {
mountPipelinesView() {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const { CommitPipelinesTable } = gl;
+ const { CommitPipelinesTable, mrWidgetData } = gl;
+
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
@@ -341,6 +342,9 @@ export default class MergeRequestTabs {
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
+ canRunPipeline: true,
+ projectId: pipelineTableViewEl.dataset.projectId,
+ mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
},
}).$mount();
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 88454c3fb4c..358f49deb35 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions } from 'vuex';
import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
import getDiscussion from 'ee_else_ce/notes/mixins/get_discussion';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index d036ff07d89..f32392c9e29 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -2,6 +2,7 @@ import initAvatarPicker from '~/avatar_picker';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
+import setupTransferEdit from '~/transfer_edit';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import { GROUP_BADGE } from '~/badges/constants';
@@ -17,6 +18,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
);
mountBadgeSettings(GROUP_BADGE);
+ setupTransferEdit('.js-group-transfer-form', '#new_parent_group_id');
// Initialize Subgroups selector
groupsSelect();
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 92ed6a652d7..c9dbe576c4b 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,6 +1,6 @@
import { PROJECT_BADGE } from '~/badges/constants';
import initSettingsPanels from '~/settings_panels';
-import setupProjectEdit from '~/project_edit';
+import setupTransferEdit from '~/transfer_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
@@ -16,7 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
initProjectLoadingSpinner();
initProjectPermissionsSettings();
- setupProjectEdit();
+ setupTransferEdit('.js-project-transfer-form', 'select.select2');
dirtySubmitFactory(
document.querySelectorAll(
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 126a9a47a2b..876b30299fb 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,7 +1,7 @@
import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '../../locale';
-import Flash from '../../flash';
+import createFlash from '../../flash';
import Poll from '../../lib/utils/poll';
import EmptyState from '../components/empty_state.vue';
import SvgBlankState from '../components/blank_state.vue';
@@ -62,6 +62,7 @@ export default {
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
+ eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
@@ -69,6 +70,7 @@ export default {
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
+ eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
destroyed() {
this.poll.stop();
@@ -110,7 +112,7 @@ export default {
// Stop polling
this.poll.stop();
// Restarting the poll also makes an initial request
- this.poll.restart();
+ return this.poll.restart();
},
fetchPipelines() {
if (!this.isMakingRequest) {
@@ -156,7 +158,31 @@ export default {
this.service
.postAction(endpoint)
.then(() => this.updateTable())
- .catch(() => Flash(__('An error occurred while making the request.')));
+ .catch(() => createFlash(__('An error occurred while making the request.')));
+ },
+
+ /**
+ * When the user clicks on the run pipeline button
+ * we toggle the state of the button to be disabled
+ *
+ * Once the post request has finished, we fetch the
+ * pipelines again to show the most recent data
+ *
+ * Once the pipeline has been updated, we toggle back the
+ * loading state and re-enable the run pipeline button
+ */
+ runMergeRequestPipeline(options) {
+ this.store.toggleIsRunningPipeline(true);
+
+ this.service
+ .runMRPipeline(options)
+ .then(() => this.updateTable())
+ .catch(() => {
+ createFlash(
+ __('An error occurred while trying to run a new pipeline for this Merge Request.'),
+ );
+ })
+ .finally(() => this.store.toggleIsRunningPipeline(false));
},
},
};
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 8317d3f4510..3c755db23dc 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -1,4 +1,5 @@
import axios from '../../lib/utils/axios_utils';
+import Api from '~/api';
export default class PipelinesService {
/**
@@ -39,4 +40,9 @@ export default class PipelinesService {
postAction(endpoint) {
return axios.post(`${endpoint}.json`);
}
+
+ // eslint-disable-next-line class-methods-use-this
+ runMRPipeline({ projectId, mergeRequestId }) {
+ return Api.postMergeRequestPipeline(projectId, { mergeRequestId });
+ }
}
diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
index 651251d2623..a4bbada89c8 100644
--- a/app/assets/javascripts/pipelines/stores/pipelines_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
@@ -7,6 +7,9 @@ export default class PipelinesStore {
this.state.pipelines = [];
this.state.count = {};
this.state.pageInfo = {};
+
+ // Used in MR Pipelines tab
+ this.state.isRunningMergeRequestPipeline = false;
}
storePipelines(pipelines = []) {
@@ -29,4 +32,13 @@ export default class PipelinesStore {
this.state.pageInfo = paginationInfo;
}
+
+ /**
+ * Toggles the isRunningPipeline flag
+ *
+ * @param {Boolean} value
+ */
+ toggleIsRunningPipeline(value = false) {
+ this.state.isRunningMergeRequestPipeline = value;
+ }
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index c6cc04a139f..ce592720531 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -67,18 +67,14 @@ export default {
saveAssignees() {
this.loading = true;
- function setLoadingFalse() {
- this.loading = false;
- }
-
this.mediator
.saveAssignees(this.field)
- .then(setLoadingFalse.bind(this))
.then(() => {
+ this.loading = false;
refreshUserMergeRequestCounts();
})
.catch(() => {
- setLoadingFalse();
+ this.loading = false;
return new Flash(__('Error occurred when saving assignees'));
});
},
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index cbe20f761ff..feb08e3acaf 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,7 +1,4 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class SidebarService {
constructor(endpointMap) {
@@ -18,23 +15,15 @@ export default class SidebarService {
}
get() {
- return Vue.http.get(this.endpoint);
+ return axios.get(this.endpoint);
}
update(key, data) {
- return Vue.http.put(
- this.endpoint,
- {
- [key]: data,
- },
- {
- emulateJSON: true,
- },
- );
+ return axios.put(this.endpoint, { [key]: data });
}
getProjectsAutocomplete(searchTerm) {
- return Vue.http.get(this.projectsAutocompleteEndpoint, {
+ return axios.get(this.projectsAutocompleteEndpoint, {
params: {
search: searchTerm,
},
@@ -42,11 +31,11 @@ export default class SidebarService {
}
toggleSubscription() {
- return Vue.http.post(this.toggleSubscriptionEndpoint);
+ return axios.post(this.toggleSubscriptionEndpoint);
}
moveIssue(moveToProjectId) {
- return Vue.http.post(this.moveIssueEndpoint, {
+ return axios.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId,
});
}
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 643fe6c00b6..4a7000cbbda 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -32,7 +32,10 @@ export default class SidebarMediator {
// If there are no ids, that means we have to unassign (which is id = 0)
// And it only accepts an array, hence [0]
- return this.service.update(field, selected.length === 0 ? [0] : selected);
+ const assignees = selected.length === 0 ? [0] : selected;
+ const data = { assignee_ids: assignees };
+
+ return this.service.update(field, data);
}
setMoveToProjectId(projectId) {
@@ -42,8 +45,7 @@ export default class SidebarMediator {
fetch() {
return this.service
.get()
- .then(response => response.json())
- .then(data => {
+ .then(({ data }) => {
this.processFetchedData(data);
})
.catch(() => new Flash(__('Error occurred when fetching sidebar data')));
@@ -71,23 +73,17 @@ export default class SidebarMediator {
}
fetchAutocompleteProjects(searchTerm) {
- return this.service
- .getProjectsAutocomplete(searchTerm)
- .then(response => response.json())
- .then(data => {
- this.store.setAutocompleteProjects(data);
- return this.store.autocompleteProjects;
- });
+ return this.service.getProjectsAutocomplete(searchTerm).then(({ data }) => {
+ this.store.setAutocompleteProjects(data);
+ return this.store.autocompleteProjects;
+ });
}
moveIssue() {
- return this.service
- .moveIssue(this.store.moveToProjectId)
- .then(response => response.json())
- .then(data => {
- if (window.location.pathname !== data.web_url) {
- visitUrl(data.web_url);
- }
- });
+ return this.service.moveIssue(this.store.moveToProjectId).then(({ data }) => {
+ if (window.location.pathname !== data.web_url) {
+ visitUrl(data.web_url);
+ }
+ });
}
}
diff --git a/app/assets/javascripts/project_edit.js b/app/assets/javascripts/transfer_edit.js
index 47bf2226781..bb15e11fd4c 100644
--- a/app/assets/javascripts/project_edit.js
+++ b/app/assets/javascripts/transfer_edit.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
-export default function setupProjectEdit() {
- const $transferForm = $('.js-project-transfer-form');
- const $selectNamespace = $transferForm.find('select.select2');
+export default function setupTransferEdit(formSelector, targetSelector) {
+ const $transferForm = $(formSelector);
+ const $selectNamespace = $transferForm.find(targetSelector);
$selectNamespace.on('change', () => {
$transferForm.find(':submit').prop('disabled', !$selectNamespace.val());
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 40c095aa954..4b5201bbca7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/require-default-prop */
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import { sprintf, __ } from '~/locale';
+import { sprintf, s__ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -73,8 +73,8 @@ export default {
},
errorText() {
return sprintf(
- __(
- 'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
+ s__(
+ 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
),
{
linkStart: `<a href="${this.troubleshootingDocsPath}">`,
@@ -89,6 +89,9 @@ export default {
isMergeRequestPipeline() {
return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline);
},
+ showSourceBranch() {
+ return Boolean(this.pipeline.ref.branch);
+ },
},
};
</script>
@@ -109,7 +112,7 @@ export default {
<div class="ci-widget-content">
<div class="media-body">
<div class="font-weight-bold js-pipeline-info-container">
- {{ s__('Pipeline|Pipeline') }}
+ {{ pipeline.details.name }}
<gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
>#{{ pipeline.id }}</gl-link
>
@@ -121,48 +124,13 @@ export default {
class="commit-sha js-commit-link font-weight-normal"
>{{ pipeline.commit.short_id }}</gl-link
>
+ </template>
+ <template v-if="showSourceBranch">
{{ s__('Pipeline|on') }}
- <template v-if="isTriggeredByMergeRequest">
- <gl-link
- v-gl-tooltip
- :href="pipeline.merge_request.path"
- :title="pipeline.merge_request.title"
- class="font-weight-normal"
- >!{{ pipeline.merge_request.iid }}</gl-link
- >
- {{ s__('Pipeline|with') }}
- <tooltip-on-truncate
- :title="pipeline.merge_request.source_branch"
- truncate-target="child"
- class="label-branch label-truncate"
- >
- <gl-link
- :href="pipeline.merge_request.source_branch_path"
- class="font-weight-normal"
- >{{ pipeline.merge_request.source_branch }}</gl-link
- >
- </tooltip-on-truncate>
-
- <template v-if="isMergeRequestPipeline">
- {{ s__('Pipeline|into') }}
- <tooltip-on-truncate
- :title="pipeline.merge_request.target_branch"
- truncate-target="child"
- class="label-branch label-truncate"
- >
- <gl-link
- :href="pipeline.merge_request.target_branch_path"
- class="font-weight-normal"
- >{{ pipeline.merge_request.target_branch }}</gl-link
- >
- </tooltip-on-truncate>
- </template>
- </template>
<tooltip-on-truncate
- v-else
:title="sourceBranch"
truncate-target="child"
- class="label-branch label-truncate"
+ class="label-branch label-truncate font-weight-normal"
v-html="sourceBranchLink"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index b520d302407..326440f5013 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
@@ -9,6 +10,7 @@ import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
import icon from '../icon.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
+import axios from '~/lib/utils/axios_utils';
export default {
components: {
@@ -167,10 +169,9 @@ export default {
if (text) {
this.markdownPreviewLoading = true;
this.markdownPreview = __('Loading…');
- this.$http
+ axios
.post(this.markdownPreviewPath, { text })
- .then(resp => resp.json())
- .then(data => this.renderMarkdown(data))
+ .then(response => this.renderMarkdown(response.data))
.catch(() => new Flash(__('Error loading markdown preview')));
} else {
this.renderMarkdown();
diff --git a/app/assets/javascripts/vue_shared/plugins/global_toast.js b/app/assets/javascripts/vue_shared/plugins/global_toast.js
new file mode 100644
index 00000000000..c0de1cdc615
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/plugins/global_toast.js
@@ -0,0 +1,8 @@
+import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
+
+Vue.use(GlToast);
+
+export default function showGlobalToast(...args) {
+ return Vue.toasted.show(...args);
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 33caac4d725..ba123ff9a67 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -67,6 +67,18 @@
max-height: calc(100vh - 100px);
}
+ details {
+ margin-bottom: $gl-padding;
+
+ summary {
+ margin-bottom: $gl-padding;
+ }
+
+ *:first-child:not(summary) {
+ margin-top: $gl-padding;
+ }
+ }
+
// Single code lines should wrap
code {
font-family: $monospace-font;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 15a779dde1d..faa0a9909d5 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -726,6 +726,7 @@ $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
+$pipelines-table-header-height: 40px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index d80155a416d..e20711a193d 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -41,7 +41,6 @@
width: 20%;
}
-
.fa {
color: $cycle-analytics-light-gray;
@@ -146,7 +145,6 @@
.stage-nav-item {
line-height: 65px;
- border: 1px solid $border-color;
&.active {
background: $blue-50;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index d4bd5b1b7dc..cda6c9ce0cc 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -26,6 +26,10 @@
}
.pipelines {
+ .negative-margin-top {
+ margin-top: -$pipelines-table-header-height;
+ }
+
.stage {
max-width: 90px;
width: 90px;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 3648ec5e239..d2906ce0780 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -15,3 +15,9 @@
font-size: $size;
}
}
+
+.border-width-1px { border-width: 1px; }
+.border-style-dashed { border-style: dashed; }
+.border-style-solid { border-style: solid; }
+.border-color-blue-300 { border-color: $blue-300; }
+.border-color-default { border-color: $border-color; }
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 3648c8be426..22e629ccf59 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -7,7 +7,9 @@ class Admin::ApplicationsController < Admin::ApplicationController
before_action :load_scopes, only: [:new, :create, :edit, :update]
def index
- @applications = ApplicationsFinder.new.execute
+ applications = ApplicationsFinder.new.execute
+ @applications = Kaminari.paginate_array(applications).page(params[:page])
+ @application_counts = OauthAccessToken.distinct_resource_owner_counts(@applications)
end
def show
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 8ea77b994de..88044cf7557 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -2,6 +2,7 @@
module IssuableCollections
extend ActiveSupport::Concern
+ include PaginatedCollection
include SortingHelper
include SortingPreference
include Gitlab::IssuableMetadata
@@ -17,8 +18,11 @@ module IssuableCollections
def set_issuables_index
@issuables = issuables_collection
- set_pagination
- return if redirect_out_of_range(@total_pages)
+ unless pagination_disabled?
+ set_pagination
+
+ return if redirect_out_of_range(@issuables, @total_pages)
+ end
if params[:label_name].present? && @project
labels_params = { project_id: @project.id, title: params[:label_name] }
@@ -38,12 +42,10 @@ module IssuableCollections
end
def set_pagination
- return if pagination_disabled?
-
@issuables = @issuables.page(params[:page])
@issuables = per_page_for_relative_position if params[:sort] == 'relative_position'
@issuable_meta_data = issuable_meta_data(@issuables, collection_type, current_user)
- @total_pages = issuable_page_count
+ @total_pages = issuable_page_count(@issuables)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -57,20 +59,8 @@ module IssuableCollections
end
# rubocop: enable CodeReuse/ActiveRecord
- def redirect_out_of_range(total_pages)
- return false if total_pages.nil? || total_pages.zero?
-
- out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
- if out_of_range
- redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
- end
-
- out_of_range
- end
-
- def issuable_page_count
- page_count_for_relation(@issuables, finder.row_count) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def issuable_page_count(relation)
+ page_count_for_relation(relation, finder.row_count)
end
def page_count_for_relation(relation, row_count)
diff --git a/app/controllers/concerns/paginated_collection.rb b/app/controllers/concerns/paginated_collection.rb
new file mode 100644
index 00000000000..be84215a9e2
--- /dev/null
+++ b/app/controllers/concerns/paginated_collection.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module PaginatedCollection
+ extend ActiveSupport::Concern
+
+ private
+
+ def redirect_out_of_range(collection, total_pages = collection.total_pages)
+ return false if total_pages.zero?
+
+ out_of_range = collection.current_page > total_pages
+
+ if out_of_range
+ redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
+ end
+
+ out_of_range
+ end
+end
diff --git a/app/controllers/concerns/sessionless_authentication.rb b/app/controllers/concerns/sessionless_authentication.rb
index 4304b8565ce..ba06384a37a 100644
--- a/app/controllers/concerns/sessionless_authentication.rb
+++ b/app/controllers/concerns/sessionless_authentication.rb
@@ -2,10 +2,10 @@
# == SessionlessAuthentication
#
-# Controller concern to handle PAT and RSS token authentication methods
+# Controller concern to handle PAT, RSS, and static objects token authentication methods
#
module SessionlessAuthentication
- # This filter handles personal access tokens, and atom requests with rss tokens
+ # This filter handles personal access tokens, atom requests with rss tokens, and static object tokens
def authenticate_sessionless_user!(request_format)
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format)
diff --git a/app/controllers/concerns/static_object_external_storage.rb b/app/controllers/concerns/static_object_external_storage.rb
new file mode 100644
index 00000000000..dbfe0ed3adf
--- /dev/null
+++ b/app/controllers/concerns/static_object_external_storage.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module StaticObjectExternalStorage
+ extend ActiveSupport::Concern
+
+ included do
+ include ApplicationHelper
+ end
+
+ def redirect_to_external_storage
+ return if external_storage_request?
+
+ redirect_to external_storage_url_or_path(request.fullpath, project)
+ end
+
+ def external_storage_request?
+ header_token = request.headers['X-Gitlab-External-Storage-Token']
+ return false unless header_token.present?
+
+ external_storage_token = Gitlab::CurrentSettings.static_objects_external_storage_auth_token
+ ActiveSupport::SecurityUtils.secure_compare(header_token, external_storage_token) ||
+ raise(Gitlab::Access::AccessDeniedError)
+ end
+end
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index 161c22046f9..6feade3df03 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,14 +1,19 @@
# frozen_string_literal: true
class Dashboard::SnippetsController < Dashboard::ApplicationController
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
+
skip_cross_project_access_check :index
def index
- @snippets = SnippetsFinder.new(
- current_user,
- author: current_user,
- scope: params[:scope]
- ).execute
- @snippets = @snippets.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: current_user, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 8f6fcb362d2..940d1482611 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -2,6 +2,7 @@
class Dashboard::TodosController < Dashboard::ApplicationController
include ActionView::Helpers::NumberHelper
+ include PaginatedCollection
before_action :authorize_read_project!, only: :index
before_action :authorize_read_group!, only: :index
@@ -12,7 +13,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
@todos = @todos.page(params[:page])
@todos = @todos.with_entity_associations
- return if redirect_out_of_range(@todos)
+ return if redirect_out_of_range(@todos, todos_page_count(@todos))
end
def destroy
@@ -82,28 +83,15 @@ class Dashboard::TodosController < Dashboard::ApplicationController
}
end
- def todo_params
- params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def redirect_out_of_range(todos)
- total_pages =
- if todo_params.except(:sort, :page).empty?
- (current_user.todos_pending_count.to_f / todos.limit_value).ceil
- else
- todos.total_pages
- end
-
- return false if total_pages.zero?
-
- out_of_range = todos.current_page > total_pages
-
- if out_of_range
- redirect_to url_for(safe_params.merge(page: total_pages, only_path: true))
+ def todos_page_count(todos)
+ if todo_params.except(:sort, :page).empty? # rubocop: disable CodeReuse/ActiveRecord
+ (current_user.todos_pending_count.to_f / todos.limit_value).ceil
+ else
+ todos.total_pages
end
+ end
- out_of_range
+ def todo_params
+ params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index 76ed142c939..d4c6aae2ca8 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,8 +1,17 @@
# frozen_string_literal: true
class Explore::SnippetsController < Explore::ApplicationController
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
+
def index
- @snippets = SnippetsFinder.new(current_user).execute
- @snippets = @snippets.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user)
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 1d16ddb1608..958a24b6c0e 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -46,6 +46,15 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_personal_access_tokens_path
end
+ def reset_static_object_token
+ Users::UpdateService.new(current_user, user: @user).execute! do |user|
+ user.reset_static_object_token!
+ end
+
+ redirect_to profile_personal_access_tokens_path,
+ notice: s_('Profiles|Static object token was successfully reset')
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id)
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index ac1c4bc7fd3..1bb21857dcf 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -2,6 +2,7 @@
class Projects::ForksController < Projects::ApplicationController
include ContinueParams
+ include RendersMemberAccess
# Authorize
before_action :whitelist_query_limiting, only: [:create]
@@ -11,14 +12,16 @@ class Projects::ForksController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
- base_query = project.forks.includes(:creator)
+ @total_forks_count = project.forks.size
+ @public_forks_count = project.forks.public_only.size
+ @private_forks_count = @total_forks_count - project.forks.public_and_internal_only.size
+ @internal_forks_count = @total_forks_count - @public_forks_count - @private_forks_count
- forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
- @total_forks_count = base_query.size
- @private_forks_count = @total_forks_count - forks.size
- @public_forks_count = @total_forks_count - @private_forks_count
+ @forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
+ @forks = @forks.includes(:route, :creator, :group, namespace: [:route, :owner])
+ .page(params[:page])
- @forks = forks.page(params[:page])
+ prepare_projects_for_rendering(@forks)
respond_to do |format|
format.html
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index a51759641e4..d69f9e65874 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -2,6 +2,9 @@
class Projects::RepositoriesController < Projects::ApplicationController
include ExtractsPath
+ include StaticObjectExternalStorage
+
+ prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) }
# Authorize
before_action :require_non_empty_project, except: :create
@@ -9,6 +12,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
before_action :assign_append_sha, only: :archive
before_action :authorize_download_code!
before_action :authorize_admin_project!, only: :create
+ before_action :redirect_to_external_storage, only: :archive, if: :static_objects_external_storage_enabled?
def create
@project.create_repository
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 59f948959d6..dbd11c8ddc8 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -6,6 +6,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SpammableActions
include SnippetsActions
include RendersBlob
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
@@ -28,15 +30,14 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new(
- current_user,
- project: @project,
- scope: params[:scope]
- ).execute
- @snippets = @snippets.page(params[:page])
- if @snippets.out_of_range? && @snippets.total_pages != 0
- redirect_to project_snippets_path(@project, page: @snippets.total_pages)
- end
+ @snippets = SnippetsFinder.new(current_user, project: @project, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
def new
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 869655e9550..5805d068e21 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -7,6 +7,8 @@ class SnippetsController < ApplicationController
include SnippetsActions
include RendersBlob
include PreviewMarkdown
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
@@ -32,7 +34,13 @@ class SnippetsController < ApplicationController
@user = UserFinder.new(params[:username]).find_by_username!
@snippets = SnippetsFinder.new(current_user, author: @user, scope: params[:scope])
- .execute.page(params[:page])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
render 'index'
else
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 91e0efcf45f..e38d4073de3 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -4,6 +4,7 @@ class UsersController < ApplicationController
include RoutableActions
include RendersMemberAccess
include ControllerWithCrossProjectAccessCheck
+ include Gitlab::NoteableMetadata
requires_cross_project_access show: false,
groups: false,
@@ -165,11 +166,12 @@ class UsersController < ApplicationController
end
def load_snippets
- @snippets = SnippetsFinder.new(
- current_user,
- author: user,
- scope: params[:scope]
- ).execute.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: user, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
def build_canonical_path(user)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index ffa5719fefb..1671aa5bd04 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -169,6 +169,25 @@ module ApplicationHelper
Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
+ def static_objects_external_storage_enabled?
+ Gitlab::CurrentSettings.static_objects_external_storage_enabled?
+ end
+
+ def external_storage_url_or_path(path, project = @project)
+ return path unless static_objects_external_storage_enabled?
+
+ uri = URI(Gitlab::CurrentSettings.static_objects_external_storage_url)
+ path = URI(path) # `path` could have query parameters, so we need to split query and path apart
+
+ query = Rack::Utils.parse_nested_query(path.query)
+ query['token'] = current_user.static_object_token unless project.public?
+
+ uri.path = path.path
+ uri.query = query.to_query unless query.empty?
+
+ uri.to_s
+ end
+
def page_filter_path(options = {})
without = options.delete(:without)
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index b1a6e988a1d..93e282e44be 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -168,6 +168,8 @@ module ApplicationSettingsHelper
:asset_proxy_secret_key,
:asset_proxy_url,
:asset_proxy_whitelist,
+ :static_objects_external_storage_auth_token,
+ :static_objects_external_storage_url,
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index e990e425cb6..09866ca75ff 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -103,7 +103,7 @@ module EventsHelper
words << "at"
end
- words << event.project_name
+ words << event.resource_parent_name
words.join(" ")
end
@@ -223,3 +223,5 @@ module EventsHelper
end
end
end
+
+EventsHelper.prepend_if_ee('EE::EventsHelper')
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
new file mode 100644
index 00000000000..4d9fe345edf
--- /dev/null
+++ b/app/helpers/releases_helper.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ReleasesHelper
+ IMAGE_PATH = 'illustrations/releases.svg'
+ DOCUMENTATION_PATH = 'user/project/releases/index'
+
+ def illustration
+ image_path(IMAGE_PATH)
+ end
+
+ def help_page
+ help_page_path(DOCUMENTATION_PATH)
+ end
+
+ def url_for_merge_requests
+ project_merge_requests_url(@project, params_for_issue_and_mr_paths)
+ end
+
+ def url_for_issues
+ project_issues_url(@project, params_for_issue_and_mr_paths)
+ end
+
+ def data_for_releases_page
+ {
+ project_id: @project.id,
+ illustration_path: illustration,
+ documentation_path: help_page,
+ merge_requests_url: url_for_merge_requests,
+ issues_url: url_for_issues
+ }
+ end
+
+ private
+
+ def params_for_issue_and_mr_paths
+ { scope: 'all', state: 'opened' }
+ end
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index e39d655325f..c9cd0140ed8 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -8,6 +8,7 @@ class ApplicationSetting < ApplicationRecord
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token
+ add_authentication_token_field :static_objects_external_storage_auth_token
belongs_to :instance_administration_project, class_name: "Project"
@@ -31,15 +32,6 @@ class ApplicationSetting < ApplicationRecord
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
- self.ignored_columns += %i[
- clientside_sentry_dsn
- clientside_sentry_enabled
- koding_enabled
- koding_url
- sentry_dsn
- sentry_enabled
- ]
-
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
@@ -211,6 +203,13 @@ class ApplicationSetting < ApplicationRecord
allow_blank: false,
if: :asset_proxy_enabled?
+ validates :static_objects_external_storage_url,
+ addressable_url: true, allow_blank: true
+
+ validates :static_objects_external_storage_auth_token,
+ presence: true,
+ if: :static_objects_external_storage_url?
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index f402c0e2775..8d9597aa5a4 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -306,6 +306,10 @@ module ApplicationSettingImplementation
archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds
end
+ def static_objects_external_storage_enabled?
+ static_objects_external_storage_url.present?
+ end
+
private
def array_to_string(arr)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d2271c1335c..4aaabed6b7b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -835,12 +835,12 @@ module Ci
return unless merge_request_event?
strong_memoize(:merge_request_event_type) do
- if detached_merge_request_pipeline?
- :detached
+ if merge_train_pipeline?
+ :merge_train
elsif merge_request_pipeline?
:merged_result
- elsif merge_train_pipeline?
- :merge_train
+ elsif detached_merge_request_pipeline?
+ :detached
end
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 6a44bc7c401..b3e4df730b4 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -3,6 +3,10 @@
module Noteable
extend ActiveSupport::Concern
+ # This object is used to gather noteable meta data for list displays
+ # avoiding n+1 queries and improving performance.
+ NoteableMeta = Struct.new(:user_notes_count)
+
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
diff --git a/app/models/event.rb b/app/models/event.rb
index 52d54be39a9..580bb770599 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -3,6 +3,8 @@
class Event < ApplicationRecord
include Sortable
include FromUnion
+ include Presentable
+
default_scope { reorder(nil) }
CREATED = 1
@@ -135,6 +137,10 @@ class Event < ApplicationRecord
end
end
+ def present
+ super(presenter_class: ::EventPresenter)
+ end
+
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def visible_to_user?(user = nil)
@@ -161,12 +167,8 @@ class Event < ApplicationRecord
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
- def project_name
- if project
- project.full_name
- else
- "(deleted project)"
- end
+ def resource_parent
+ project || group
end
def target_title
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 901ebcf249f..74f8067db0a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1239,7 +1239,7 @@ class MergeRequest < ApplicationRecord
end
def compare_reports(service_class, current_user = nil)
- with_reactive_cache(service_class.name) do |data|
+ with_reactive_cache(service_class.name, current_user&.id) do |data|
unless service_class.new(project, current_user)
.latest?(base_pipeline, actual_head_pipeline, data)
raise InvalidateReactiveCache
@@ -1249,12 +1249,13 @@ class MergeRequest < ApplicationRecord
end || { status: :parsing }
end
- def calculate_reactive_cache(identifier, *args)
+ def calculate_reactive_cache(identifier, current_user_id = nil, *args)
service_class = identifier.constantize
raise NameError, service_class unless service_class < Ci::CompareReportsBaseService
- service_class.new(project).execute(base_pipeline, actual_head_pipeline)
+ current_user = User.find_by(id: current_user_id)
+ service_class.new(project, current_user).execute(base_pipeline, actual_head_pipeline)
end
def all_commits
diff --git a/app/models/note.rb b/app/models/note.rb
index 5bd3a7f969a..62b3f47fadd 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -33,8 +33,6 @@ class Note < ApplicationRecord
end
end
- self.ignored_columns += %i[original_discussion_id]
-
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
redact_field :note
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 637c017a342..bf2aec74ec8 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class NotificationSetting < ApplicationRecord
- self.ignored_columns += %i[events]
-
enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }
default_value_for :level, NotificationSetting.levels[:global]
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index 0aa920fa828..9789d8ed62b 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -6,6 +6,8 @@ class OauthAccessToken < Doorkeeper::AccessToken
alias_attribute :user, :resource_owner
+ scope :distinct_resource_owner_counts, ->(applications) { where(application: applications).distinct.group(:application_id).count(:resource_owner_id) }
+
def scopes=(value)
if value.is_a?(Array)
super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
new file mode 100644
index 00000000000..1b3183a2a43
--- /dev/null
+++ b/app/models/pages/lookup_path.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Pages
+ class LookupPath
+ def initialize(project, domain: nil)
+ @project = project
+ @domain = domain
+ end
+
+ def project_id
+ project.id
+ end
+
+ def access_control
+ project.private_pages?
+ end
+
+ def https_only
+ domain_https = domain ? domain.https? : true
+ project.pages_https_only? && domain_https
+ end
+
+ def source
+ {
+ type: 'file',
+ path: File.join(project.full_path, 'public/')
+ }
+ end
+
+ def prefix
+ '/'
+ end
+
+ private
+
+ attr_reader :project, :domain
+ end
+end
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
new file mode 100644
index 00000000000..3a876dc06a2
--- /dev/null
+++ b/app/models/pages/virtual_domain.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Pages
+ class VirtualDomain
+ def initialize(projects, domain: nil)
+ @projects = projects
+ @domain = domain
+ end
+
+ def certificate
+ domain&.certificate
+ end
+
+ def key
+ domain&.key
+ end
+
+ def lookup_paths
+ projects.map do |project|
+ project.pages_lookup_path(domain: domain)
+ end.sort_by(&:prefix).reverse
+ end
+
+ private
+
+ attr_reader :projects, :domain
+ end
+end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index a2a471074a9..22a6bae7cf7 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -185,6 +185,10 @@ class PagesDomain < ApplicationRecord
self.certificate_source = 'gitlab_provided' if key_changed?
end
+ def pages_virtual_domain
+ Pages::VirtualDomain.new([project], domain: self)
+ end
+
private
def set_verification_code
diff --git a/app/models/project.rb b/app/models/project.rb
index d948410e397..12f5da05efa 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -61,11 +61,11 @@ class Project < ApplicationRecord
cache_markdown_field :description, pipeline: :description
- delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
- :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?,
- :merge_requests_access_level, :issues_access_level, :wiki_access_level,
- :snippets_access_level, :builds_access_level, :repository_access_level,
- to: :project_feature, allow_nil: true
+ delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?,
+ :issues_enabled?, :pages_enabled?, :public_pages?, :private_pages?,
+ :merge_requests_access_level, :issues_access_level, :wiki_access_level,
+ :snippets_access_level, :builds_access_level, :repository_access_level,
+ to: :project_feature, allow_nil: true
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
@@ -2201,6 +2201,10 @@ class Project < ApplicationRecord
members.maintainers.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
+ def pages_lookup_path(domain: nil)
+ Pages::LookupPath.new(self, domain: domain)
+ end
+
private
def merge_requests_allowing_collaboration(source_branch = nil)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 78e82955342..efa3fbcf015 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -129,6 +129,10 @@ class ProjectFeature < ApplicationRecord
pages_access_level == PUBLIC || pages_access_level == ENABLED && project.public?
end
+ def private_pages?
+ !public_pages?
+ end
+
private
# Validates builds and merge requests access level
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5cb4b56a114..e5a83366776 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1134,6 +1134,10 @@ class Repository
@cache ||= Gitlab::RepositoryCache.new(self)
end
+ def redis_set_cache
+ @redis_set_cache ||= Gitlab::RepositorySetCache.new(self)
+ end
+
def request_store_cache
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 00931457344..b2fca65b9e0 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -55,6 +55,7 @@ class Snippet < ApplicationRecord
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
+ scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> { includes(author: :status) }
participant :author
diff --git a/app/models/user.rb b/app/models/user.rb
index 5f109feb96a..48acdfeb2ed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -23,14 +23,9 @@ class User < ApplicationRecord
DEFAULT_NOTIFICATION_LEVEL = :participating
- self.ignored_columns += %i[
- authentication_token
- email_provider
- external_email
- ]
-
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
+ add_authentication_token_field :static_object_token
default_value_for :admin, false
default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
@@ -61,6 +56,9 @@ class User < ApplicationRecord
BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
"administrator if you think this is an error."
+ # Removed in GitLab 12.3. Keep until after 2019-09-22.
+ self.ignored_columns += %i[support_bot]
+
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
# rubocop: disable CodeReuse/ServiceClass
@@ -1437,6 +1435,13 @@ class User < ApplicationRecord
ensure_feed_token!
end
+ # Each existing user needs to have a `static_object_token`.
+ # We do this on read since migrating all existing users is not a feasible
+ # solution.
+ def static_object_token
+ ensure_static_object_token!
+ end
+
def sync_attribute?(attribute)
return true if ldap_user? && attribute == :email
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
new file mode 100644
index 00000000000..f31d362d5fa
--- /dev/null
+++ b/app/presenters/event_presenter.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class EventPresenter < Gitlab::View::Presenter::Delegated
+ presents :event
+
+ def resource_parent_name
+ resource_parent&.full_name || ''
+ end
+
+ def target_link_options
+ case resource_parent
+ when Group
+ [event.group, event.target]
+ when Project
+ [event.project.namespace.becomes(Namespace), event.project, event.target]
+ else
+ ''
+ end
+ end
+end
diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb
index 6c2d80d8f45..5b76e1824e4 100644
--- a/app/services/ci/compare_reports_base_service.rb
+++ b/app/services/ci/compare_reports_base_service.rb
@@ -41,7 +41,7 @@ module Ci
end
def serializer_params
- { project: project }
+ { project: project, current_user: current_user }
end
def get_report(pipeline)
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 9d4cf5df713..21055ad6617 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -6,7 +6,7 @@ module Ci
class RegisterJobService
attr_reader :runner
- JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30, 60, 300, 900].freeze
+ JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30, 60, 300, 900, 1800, 3600].freeze
JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
Result = Struct.new(:build, :valid?)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 308a3a10d1a..88ed0c3ef4c 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -33,7 +33,8 @@ module MergeRequests
merge_request.assign_attributes(params.to_h.compact)
merge_request.compare_commits = []
- merge_request.target_branch = find_target_branch
+ set_merge_request_target_branch
+
merge_request.can_be_created = projects_and_branches_valid?
# compare branches only if branches are valid, otherwise
@@ -93,8 +94,12 @@ module MergeRequests
project_from_params
end
- def find_target_branch
- target_branch || target_project.default_branch
+ def set_merge_request_target_branch
+ if source_branch_default? && !target_branch_specified?
+ merge_request.target_branch = nil
+ else
+ merge_request.target_branch ||= target_project.default_branch
+ end
end
def source_branch_specified?
@@ -149,7 +154,15 @@ module MergeRequests
end
def same_source_and_target?
- source_project == target_project && target_branch == source_branch
+ same_source_and_target_project? && target_branch == source_branch
+ end
+
+ def source_branch_default?
+ same_source_and_target_project? && source_branch == target_project.default_branch
+ end
+
+ def same_source_and_target_project?
+ source_project == target_project
end
def source_branch_exists?
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index f711839e389..18a90c952fa 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -18,7 +18,7 @@ module Search
end
def projects
- @projects ||= ProjectsFinder.new(current_user: current_user).execute
+ @projects ||= ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute
end
def allowed_scopes
diff --git a/app/views/admin/application_settings/_repository_static_objects.html.haml b/app/views/admin/application_settings/_repository_static_objects.html.haml
new file mode 100644
index 00000000000..03aa48b2282
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_static_objects.html.haml
@@ -0,0 +1,18 @@
+= form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-repository-static-objects-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :static_objects_external_storage_url, class: 'label-bold' do
+ = _('External storage URL')
+ = f.text_field :static_objects_external_storage_url, class: 'form-control'
+ %span.form-text.text-muted#static_objects_external_storage_url_help_block
+ = _('URL of the external storage that will serve the repository static objects (e.g. archives, blobs, ...).')
+ .form-group
+ = f.label :static_objects_external_storage_auth_token, class: 'label-bold' do
+ = _('External storage authentication token')
+ = f.text_field :static_objects_external_storage_auth_token, class: 'form-control'
+ %span.form-text.text-muted#static_objects_external_storage_auth_token_help_block
+ = _('A secure token that identifies an external storage request.')
+
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index b50a0dd5a18..25f8b6541b5 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -34,3 +34,14 @@
= _('Configure automatic git checks and housekeeping on repositories.')
.settings-content
= render 'repository_check'
+
+%section.settings.as-repository-static-objects.no-animate#js-repository-static-objects-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Repository static objects')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Serve repository static objects (e.g. archives, blobs, ...) from an external storage (e.g. a CDN).')
+ .settings-content
+ = render 'repository_static_objects'
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index 2cdf98075d1..758d722cc63 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -19,7 +19,8 @@
%tr{ :id => "application_#{application.id}" }
%td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri
- %td= application.access_tokens.map(&:resource_owner_id).uniq.count
+ %td= @application_counts[application.id].to_i
%td= application.trusted? ? 'Y': 'N'
%td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link'
%td= render 'delete_form', application: application
+= paginate @applications, theme: 'gitlab'
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 5507f12b73b..a6acf948ed4 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,5 +1,5 @@
- more_info_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
- anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank'
+ anchor: 'add-existing-kubernetes-cluster'), target: '_blank'
- rbac_help_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
diff --git a/app/views/clusters/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml
index 749177fa6c1..3b9ceaa2b8a 100644
--- a/app/views/clusters/clusters/user/_header.html.haml
+++ b/app/views/clusters/clusters/user/_header.html.haml
@@ -1,5 +1,5 @@
%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
+ - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'add-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index d56234e6c1a..406e8a93194 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -1,5 +1,7 @@
return unless event.visible_to_user?(current_user)
+event = event.present
+
xml.entry do
xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
xml.link href: event_feed_url(event)
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 222175c818a..647f0597adb 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,3 +1,5 @@
+- event = event.present
+
- if event.visible_to_user?(current_user)
.event-item
.event-item-timestamp
diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml
index 98941722434..67e4c538b4a 100644
--- a/app/views/events/_event_scope.html.haml
+++ b/app/views/events/_event_scope.html.haml
@@ -2,6 +2,5 @@
= event_preposition(event)
- if event.project
= link_to_project(event.project)
- - else
- = event.project_name
-
+ - elsif event.group
+ = link_to event.resource_parent_name, group_path(event.group)
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index b02fdb4b638..50c5885c648 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -8,7 +8,7 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name
%span.event-target-type.append-right-4= event.target_type.titleize.downcase
- = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
+ = link_to event.target_link_options, class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
= event.target.reference_link_text
- unless event.milestone?
%span.event-target-title.append-right-4{ dir: "auto" }
@@ -17,4 +17,4 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event_action_name(event)
- = render "events/event_scope", event: event
+ = render "events/event_scope", event: event if event.resource_parent.present?
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 2f156603414..606b0febb57 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -10,4 +10,4 @@
- if event.project
= link_to_project(event.project)
- else
- = event.project_name
+ = event.resource_parent_name
diff --git a/app/views/graphiql/rails/editors/show.html.erb b/app/views/graphiql/rails/editors/show.html.erb
new file mode 100644
index 00000000000..abb1ed0e772
--- /dev/null
+++ b/app/views/graphiql/rails/editors/show.html.erb
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><%= GraphiQL::Rails.config.title || 'GraphiQL' %></title>
+
+ <%= stylesheet_link_tag("graphiql/rails/application") %>
+ <%= javascript_include_tag("graphiql/rails/application", nonce: true) %>
+ </head>
+ <body>
+ <%= content_tag :div, 'Loading...', id: 'graphiql-container', data: {
+ graphql_endpoint_path: graphql_endpoint_path,
+ initial_query: GraphiQL::Rails.config.initial_query,
+ logo: GraphiQL::Rails.config.logo,
+ headers: GraphiQL::Rails.config.resolve_headers(self),
+ query_params: GraphiQL::Rails.config.query_params
+ } %>
+ </body>
+</html>
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index d1eb6478997..64fec260f3b 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -25,7 +25,7 @@
.sub-section
%h4.warning-title Transfer group
- = form_for @group, url: transfer_group_path(@group), method: :put do |f|
+ = form_for @group, url: transfer_group_path(@group), method: :put, html: { class: 'js-group-transfer-form' } do |f|
.form-group
= dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: 'Search groups', data: { data: parent_group_options(@group) } })
= hidden_field_tag 'new_parent_group_id'
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index 4a6d8a1870d..8b862522645 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -1,7 +1,7 @@
- is_admin = local_assigns.fetch(:admin, false)
- if @keys.any?
- %ul.content-list
+ %ul.content-list{ data: { qa_selector: 'ssh_keys_list' } }
= render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin }
- else
%p.settings-message.text-center
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 08a39fc4f58..d9e94908b80 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -54,3 +54,23 @@
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
= reset_message.html_safe
+
+- if static_objects_external_storage_enabled?
+ %hr
+ .row.prepend-top-default
+ .col-lg-4
+ %h4.prepend-top-0
+ = s_('AccessTokens|Static object token')
+ %p
+ = s_('AccessTokens|Your static object token is used to authenticate you when repository static objects (e.g. archives, blobs, ...) are being served from an external storage.')
+ %p
+ = s_('AccessTokens|It cannot be used to access any other data.')
+ .col-lg-8
+ = label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
+ = text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.form-text.text-muted
+ - reset_link = url_for [:reset, :static_object_token, :profile]
+ - reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
+ - reset_link_end = '</a>'.html_safe
+ - reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can access repository static objects as if they were you. You should %{reset_link_start}reset it%{reset_link_end} if that ever happens.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
+ = reset_message.html_safe
diff --git a/app/views/projects/buttons/_download_links.html.haml b/app/views/projects/buttons/_download_links.html.haml
index d344167a6c5..b256d94065b 100644
--- a/app/views/projects/buttons/_download_links.html.haml
+++ b/app/views/projects/buttons/_download_links.html.haml
@@ -2,4 +2,5 @@
.btn-group.ml-0.w-100
- formats.each do |(fmt, extra_class)|
- = link_to fmt, project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
+ - archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
+ = link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 68b35072f26..81c354f1c8f 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -5,4 +5,5 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
+ "project-id": @project.id,
} }
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 0397a7034c7..8384561891a 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -1,6 +1,6 @@
.top-area
.nav-text
- - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
+ - full_count_title = "#{@public_forks_count} public, #{@internal_forks_count} internal, and #{@private_forks_count} private"
#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
.nav-controls
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index be01905dd35..c6615b26bc0 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -51,7 +51,7 @@
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
+ = dropdown_toggle f.object.target_branch || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select target branch"))
= dropdown_filter(_("Search branches"))
diff --git a/app/views/projects/releases/index.html.haml b/app/views/projects/releases/index.html.haml
index 326b83c856e..4d5b8cc80f7 100644
--- a/app/views/projects/releases/index.html.haml
+++ b/app/views/projects/releases/index.html.haml
@@ -1,3 +1,3 @@
- page_title _('Releases')
-#js-releases-page{ data: { project_id: @project.id, illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/project/releases/index') } }
+#js-releases-page{ data: data_for_releases_page }
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 51b7f2dd4b4..ebd99cf8605 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -26,7 +26,7 @@
= (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe
.prepend-top-default.append-bottom-default
- .md.md-file
+ .md.md-file{ data: { qa_selector: 'wiki_page_content' } }
= render_wiki_content(@page)
= render 'sidebar'
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 1dc538826dc..dfb0e7ed297 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,7 +1,7 @@
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
-#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
+#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}", signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
= icon('spinner spin')
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index bb05658c719..d70a1631010 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -42,12 +42,6 @@
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, merge_requests: merge_requests,
issues: issues, pipeline_status: pipeline_status, compact_mode: compact_mode
-
- - if @private_forks_count && @private_forks_count > 0
- %li.project-row.private-forks-notice
- = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
- %strong= pluralize(@private_forks_count, 'private fork')
- %span &nbsp;you have no access to.
= paginate_collection(projects, remote: remote) unless skip_pagination
- else
- if @contributed_projects
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index ebb634fe75f..1a9ae68f53d 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -17,7 +17,7 @@
= render "snippets/actions"
.snippet-header.limited-header-width
- %h2.snippet-title.prepend-top-0.append-bottom-0.qa-snippet-title
+ %h2.snippet-title.prepend-top-0.mb-3.qa-snippet-title
= markdown_field(@snippet, :title)
- if @snippet.description.present?
diff --git a/app/views/shared/snippets/_list.html.haml b/app/views/shared/snippets/_list.html.haml
index 5d2152eb411..766f48fff3d 100644
--- a/app/views/shared/snippets/_list.html.haml
+++ b/app/views/shared/snippets/_list.html.haml
@@ -1,12 +1,11 @@
- remote = local_assigns.fetch(:remote, false)
- link_project = local_assigns.fetch(:link_project, false)
-- if @snippets.exists?
+- if @snippets.to_a.empty?
+ .nothing-here-block= s_("SnippetsEmptyState|No snippets found")
+- else
.snippets-list-holder
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
= paginate @snippets, theme: 'gitlab', remote: remote
-
-- else
- .nothing-here-block= s_("SnippetsEmptyState|No snippets found")
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 42af97bc6af..0ef626868a2 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,4 +1,5 @@
- link_project = local_assigns.fetch(:link_project, false)
+- notes_count = @noteable_meta_data[snippet.id].user_notes_count
%li.snippet-row
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
@@ -12,10 +13,9 @@
%ul.controls
%li
- - note_count = snippet.notes.user.count
- = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
+ = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count.zero?) do
= icon('comments')
- = note_count
+ = notes_count
%li
%span.sr-only
= visibility_level_label(snippet.visibility_level)
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 3191eaa1e2c..7516dfe1602 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -27,7 +27,7 @@
- if event.project
= link_to_project(event.project)
- else
- = event.project_name
+ = event.resource_parent_name
- else
made a private contribution
- else