diff options
Diffstat (limited to 'app')
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 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 |