diff options
author | James Lopez <james@jameslopez.es> | 2018-04-06 10:42:19 +0200 |
---|---|---|
committer | James Lopez <james@jameslopez.es> | 2018-04-06 10:42:19 +0200 |
commit | a63c76faa09b5c4b7d10a9beebd46a117b10648a (patch) | |
tree | 1fe39ff16a07660ca5f214c5f08e028f6a798060 /app | |
parent | 8ce1526652ec1feb818ef9a5558826987645fa19 (diff) | |
parent | 2faf991f31aceb1c34a3855695d25fab19203e36 (diff) | |
download | gitlab-ce-a63c76faa09b5c4b7d10a9beebd46a117b10648a.tar.gz |
Merge remote-tracking branch 'origin/master' into 10-7-stable-prepare-rc210-7-stable-prepare-rc2
Diffstat (limited to 'app')
70 files changed, 771 insertions, 356 deletions
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index d22869466c9..1c237c0ec97 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex'; import ideSidebar from './ide_side_bar.vue'; import ideContextbar from './ide_context_bar.vue'; import repoTabs from './repo_tabs.vue'; -import repoFileButtons from './repo_file_buttons.vue'; import ideStatusBar from './ide_status_bar.vue'; import repoEditor from './repo_editor.vue'; @@ -12,7 +11,6 @@ export default { ideSidebar, ideContextbar, repoTabs, - repoFileButtons, ideStatusBar, repoEditor, }, @@ -70,9 +68,6 @@ export default { class="multi-file-edit-pane-content" :file="activeFile" /> - <repo-file-buttons - :file="activeFile" - /> <ide-status-bar :file="activeFile" /> diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue new file mode 100644 index 00000000000..6d07329df71 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue @@ -0,0 +1,83 @@ +<script> +import { __ } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + showButtons() { + return ( + this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink + ); + }, + rawDownloadButtonLabel() { + return this.file.binary ? __('Download') : __('Raw'); + }, + }, +}; +</script> + +<template> + <div + v-if="showButtons" + class="pull-right ide-btn-group" + > + <a + v-tooltip + :href="file.blamePath" + :title="__('Blame')" + class="btn btn-xs btn-transparent blame" + > + <icon + name="blame" + :size="16" + /> + </a> + <a + v-tooltip + :href="file.commitsPath" + :title="__('History')" + class="btn btn-xs btn-transparent history" + > + <icon + name="history" + :size="16" + /> + </a> + <a + v-tooltip + :href="file.permalink" + :title="__('Permalink')" + class="btn btn-xs btn-transparent permalink" + > + <icon + name="link" + :size="16" + /> + </a> + <a + v-tooltip + :href="file.rawPath" + target="_blank" + class="btn btn-xs btn-transparent prepend-left-10 raw" + rel="noopener noreferrer" + :title="rawDownloadButtonLabel"> + <icon + name="download" + :size="16" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index b1a16350c19..8a709b31ea0 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -2,10 +2,16 @@ /* global monaco */ import { mapState, mapGetters, mapActions } from 'vuex'; import flash from '~/flash'; +import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; +import IdeFileButtons from './ide_file_buttons.vue'; export default { + components: { + ContentViewer, + IdeFileButtons, + }, props: { file: { type: Object, @@ -13,11 +19,21 @@ export default { }, }, computed: { - ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']), + ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']), ...mapGetters(['currentMergeRequest']), shouldHideEditor() { return this.file && this.file.binary && !this.file.raw; }, + editTabCSS() { + return { + active: this.file.viewMode === 'edit', + }; + }, + previewTabCSS() { + return { + active: this.file.viewMode === 'preview', + }; + }, }, watch: { file(oldVal, newVal) { @@ -26,15 +42,17 @@ export default { this.initMonaco(); } }, - leftPanelCollapsed() { - this.editor.updateDimensions(); - }, rightPanelCollapsed() { this.editor.updateDimensions(); }, viewer() { this.createEditorInstance(); }, + panelResizing() { + if (!this.panelResizing) { + this.editor.updateDimensions(); + } + }, }, beforeDestroy() { this.editor.dispose(); @@ -56,6 +74,7 @@ export default { 'changeFileContent', 'setFileLanguage', 'setEditorPosition', + 'setFileViewMode', 'setFileEOL', 'updateViewer', 'updateDelayViewerUpdated', @@ -153,15 +172,47 @@ export default { class="blob-viewer-container blob-editor-container" > <div - v-if="shouldHideEditor" - v-html="file.html" - > + class="ide-mode-tabs clearfix" + v-if="!shouldHideEditor"> + <ul class="nav-links pull-left"> + <li :class="editTabCSS"> + <a + href="javascript:void(0);" + role="button" + @click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> + <template v-if="viewer === 'editor'"> + {{ __('Edit') }} + </template> + <template v-else> + {{ __('Review') }} + </template> + </a> + </li> + <li + v-if="file.previewMode" + :class="previewTabCSS"> + <a + href="javascript:void(0);" + role="button" + @click.prevent="setFileViewMode({ file, viewMode:'preview' })"> + {{ file.previewMode.previewTitle }} + </a> + </li> + </ul> + <ide-file-buttons + :file="file" + /> </div> <div - v-show="!shouldHideEditor" + v-show="!shouldHideEditor && file.viewMode === 'edit'" ref="editor" class="multi-file-editor-holder" > </div> + <content-viewer + v-if="!shouldHideEditor && file.viewMode === 'preview'" + :content="file.content || file.raw" + :path="file.path" + :project-path="file.projectId"/> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue deleted file mode 100644 index 4ea8cf7504b..00000000000 --- a/app/assets/javascripts/ide/components/repo_file_buttons.vue +++ /dev/null @@ -1,61 +0,0 @@ -<script> -export default { - props: { - file: { - type: Object, - required: true, - }, - }, - computed: { - showButtons() { - return this.file.rawPath || - this.file.blamePath || - this.file.commitsPath || - this.file.permalink; - }, - rawDownloadButtonLabel() { - return this.file.binary ? 'Download' : 'Raw'; - }, - }, -}; -</script> - -<template> - <div - v-if="showButtons" - class="multi-file-editor-btn-group" - > - <a - :href="file.rawPath" - target="_blank" - class="btn btn-default btn-sm raw" - rel="noopener noreferrer"> - {{ rawDownloadButtonLabel }} - </a> - - <div - class="btn-group" - role="group" - aria-label="File actions" - > - <a - :href="file.blamePath" - class="btn btn-default btn-sm blame" - > - Blame - </a> - <a - :href="file.commitsPath" - class="btn btn-default btn-sm history" - > - History - </a> - <a - :href="file.permalink" - class="btn btn-default btn-sm permalink" - > - Permalink - </a> - </div> - </div> -</template> diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 6b4ba30e086..001737d6ee8 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -69,6 +69,7 @@ export default class Editor { occurrencesHighlight: false, renderLineHighlight: 'none', hideCursorInOverviewRuler: true, + renderSideBySide: Editor.renderSideBySide(domElement), })), ); @@ -81,7 +82,7 @@ export default class Editor { } attachModel(model) { - if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') { + if (this.isDiffEditorType) { this.instance.setModel({ original: model.getOriginalModel(), modified: model.getModel(), @@ -153,6 +154,7 @@ export default class Editor { updateDimensions() { this.instance.layout(); + this.updateDiffView(); } setPosition({ lineNumber, column }) { @@ -171,4 +173,20 @@ export default class Editor { this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e))); } + + updateDiffView() { + if (!this.isDiffEditorType) return; + + this.instance.updateOptions({ + renderSideBySide: Editor.renderSideBySide(this.instance.getDomNode()), + }); + } + + get isDiffEditorType() { + return this.instance.getEditorType() === 'vs.editor.IDiffEditor'; + } + + static renderSideBySide(domElement) { + return domElement.offsetWidth >= 700; + } } diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js index a213862f9b3..9f895d49f2e 100644 --- a/app/assets/javascripts/ide/lib/editor_options.js +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -6,7 +6,7 @@ export const defaultEditorOptions = { minimap: { enabled: false, }, - wordWrap: 'bounded', + wordWrap: 'on', }; export default [ diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 6b034ea1e82..1a17320a1ea 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -149,6 +149,10 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn } }; +export const setFileViewMode = ({ state, commit }, { file, viewMode }) => { + commit(types.SET_FILE_VIEWMODE, { file, viewMode }); +}; + export const discardFileChanges = ({ state, commit }, path) => { const file = state.entries[path]; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index ee759bff516..e3f504e5ab0 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -38,6 +38,7 @@ export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA'; export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; export const SET_FILE_POSITION = 'SET_FILE_POSITION'; +export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE'; export const SET_FILE_EOL = 'SET_FILE_EOL'; export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED'; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 926b6f66d78..6a143e518f9 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -42,6 +42,7 @@ export default { renderError: data.render_error, raw: null, baseRaw: null, + html: data.html, }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { @@ -83,6 +84,11 @@ export default { mrChange, }); }, + [types.SET_FILE_VIEWMODE](state, { file, viewMode }) { + Object.assign(state.entries[file.path], { + viewMode, + }); + }, [types.DISCARD_FILE_CHANGES](state, path) { Object.assign(state.entries[path], { content: state.entries[path].raw, diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 63e4de3b17d..4befcc501ef 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -38,6 +38,8 @@ export const dataStructure = () => ({ editorColumn: 1, fileLanguage: '', eol: '', + viewMode: 'edit', + previewMode: null, }); export const decorateData = entity => { @@ -57,8 +59,9 @@ export const decorateData = entity => { changed = false, parentTreeUrl = '', base64 = false, - + previewMode, file_lock, + html, } = entity; return { @@ -79,8 +82,9 @@ export const decorateData = entity => { renderError, content, base64, - + previewMode, file_lock, + html, }; }; diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js index a4cd1ab099f..a1673276900 100644 --- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -1,14 +1,8 @@ +import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import { decorateData, sortTree } from '../utils'; self.addEventListener('message', e => { - const { - data, - projectId, - branchId, - tempFile = false, - content = '', - base64 = false, - } = e.data; + const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data; const treeList = []; let file; @@ -19,9 +13,7 @@ self.addEventListener('message', e => { if (pathSplit.length > 0) { pathSplit.reduce((pathAcc, folderName) => { const parentFolder = acc[pathAcc[pathAcc.length - 1]]; - const folderPath = `${ - parentFolder ? `${parentFolder.path}/` : '' - }${folderName}`; + const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`; const foundEntry = acc[folderPath]; if (!foundEntry) { @@ -33,9 +25,7 @@ self.addEventListener('message', e => { path: folderPath, url: `/${projectId}/tree/${branchId}/${folderPath}/`, type: 'tree', - parentTreeUrl: parentFolder - ? parentFolder.url - : `/${projectId}/tree/${branchId}/`, + parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`, tempFile, changed: tempFile, opened: tempFile, @@ -70,13 +60,12 @@ self.addEventListener('message', e => { path, url: `/${projectId}/blob/${branchId}/${path}`, type: 'blob', - parentTreeUrl: fileFolder - ? fileFolder.url - : `/${projectId}/blob/${branchId}`, + parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`, tempFile, changed: tempFile, content, base64, + previewMode: viewerInformationForPath(blobName), }); Object.assign(acc, { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b0573510ff9..ac70ddb3ff4 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1190,12 +1190,12 @@ export default class Notes { addForm = false; let lineTypeSelector = ''; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content discussion-notes"></div></td></tr>'; // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineTypeSelector = `.${lineType}`; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content discussion-notes"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content discussion-notes"></div></td></tr>'; } const notesContentSelector = `.notes_content${lineTypeSelector} .content`; let notesContent = targetRow.find(notesContentSelector); diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index e0f883a8e08..476b15aca4a 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -258,9 +258,7 @@ Please check your network connection and try again.`; :key="note.id" /> </ul> - <div - :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder"> + <div class="discussion-reply-holder"> <template v-if="!isReplying && canReply"> <div class="btn-group-justified discussion-with-resolve-btn" diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 9b136573135..d501c465a96 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -17,6 +17,7 @@ * "text": "passed", * "label": "passed", * "group": "success", + * "tooltip": "passed", * "details_path": "/root/ci-mock/builds/4256", * "action": { * "icon": "retry", @@ -69,12 +70,12 @@ textBuilder.push(this.job.name); } - if (this.job.name && this.status.label) { + if (this.job.name && this.status.tooltip) { textBuilder.push('-'); } - if (this.status.label) { - textBuilder.push(`${this.job.status.label}`); + if (this.status.tooltip) { + textBuilder.push(`${this.job.status.tooltip}`); } return textBuilder.join(' '); @@ -100,6 +101,7 @@ :title="tooltipText" :class="cssClassJobName" data-container="body" + data-html="true" class="js-pipeline-graph-job-link" > @@ -115,6 +117,7 @@ class="js-job-component-tooltip" :title="tooltipText" :class="cssClassJobName" + data-html="true" data-container="body" > diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue new file mode 100644 index 00000000000..fb8ccea91c7 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue @@ -0,0 +1,43 @@ +<script> +import { viewerInformationForPath } from './lib/viewer_utils'; +import MarkdownViewer from './viewers/markdown_viewer.vue'; + +export default { + props: { + content: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + viewer() { + const previewInfo = viewerInformationForPath(this.path); + switch (previewInfo.id) { + case 'markdown': + return MarkdownViewer; + default: + return null; + } + }, + }, +}; +</script> + +<template> + <div class="preview-container"> + <component + :is="viewer" + :project-path="projectPath" + :content="content" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js new file mode 100644 index 00000000000..4f2e1e47dd1 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -0,0 +1,23 @@ +const viewers = { + markdown: { + id: 'markdown', + previewTitle: 'Preview Markdown', + }, +}; + +const fileNameViewers = {}; +const fileExtensionViewers = { + md: 'markdown', + markdown: 'markdown', +}; + +export function viewerInformationForPath(path) { + if (!path) return null; + const name = path.split('/').pop(); + const viewerName = + fileNameViewers[name] || fileExtensionViewers[name ? name.split('.').pop() : ''] || ''; + + return viewers[viewerName]; +} + +export default viewers; diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue new file mode 100644 index 00000000000..09e0094054d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -0,0 +1,90 @@ +<script> +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import $ from 'jquery'; +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; + +const CancelToken = axios.CancelToken; +let axiosSource; + +export default { + components: { + SkeletonLoadingContainer, + }, + props: { + content: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + }, + data() { + return { + previewContent: null, + isLoading: false, + }; + }, + watch: { + content() { + this.previewContent = null; + }, + }, + created() { + axiosSource = CancelToken.source(); + this.fetchMarkdownPreview(); + }, + updated() { + this.fetchMarkdownPreview(); + }, + destroyed() { + if (this.isLoading) axiosSource.cancel('Cancelling Preview'); + }, + methods: { + fetchMarkdownPreview() { + if (this.content && this.previewContent === null) { + this.isLoading = true; + const postBody = { + text: this.content, + }; + const postOptions = { + cancelToken: axiosSource.token, + }; + + axios + .post( + `${gon.relative_url_root}/${this.projectPath}/preview_markdown`, + postBody, + postOptions, + ) + .then(({ data }) => { + this.previewContent = data.body; + this.isLoading = false; + + this.$nextTick(() => { + $(this.$refs['markdown-preview']).renderGFM(); + }); + }) + .catch(() => { + this.previewContent = __('An error occurred while fetching markdown preview'); + this.isLoading = false; + }); + } + }, + }, +}; +</script> + +<template> + <div + ref="markdown-preview" + class="md md-previewer"> + <skeleton-loading-container v-if="isLoading" /> + <div + v-else + v-html="previewContent"> + </div> + </div> +</template> diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 7e829826eba..f1a8a46dda4 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -24,6 +24,10 @@ color: $list-text-disabled-color; } + &:not(.ui-sort-disabled):hover { + background: $row-hover; + } + &.unstyled { &:hover { background: none; @@ -34,14 +38,15 @@ background-color: $list-warning-row-bg; border-color: $list-warning-row-border; color: $list-warning-row-color; - } - &.smoke { background-color: $gray-light; } + &:hover { + background: $list-warning-row-bg; + } - &:not(.ui-sort-disabled):hover { - background: $row-hover; } + &.smoke { background-color: $gray-light; } + &:last-child { border-bottom: 0; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 294c59f037f..9e1371648ed 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -289,6 +289,11 @@ body { &:last-child { margin-bottom: 0; } + + &.with-button { + line-height: 34px; + } + } .page-title-empty { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 98d460339cd..7a6352e45f1 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -391,7 +391,7 @@ } &:hover { - background-color: $row-hover; + background-color: $dropdown-item-hover-bg; } .icon-retry { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7f037582ca0..679f783b1b6 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -813,6 +813,7 @@ } .discussion-notes { + padding: 0 $gl-padding $gl-padding; min-height: 35px; &:first-child { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4a528bc2bb1..8720f821ce9 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -173,11 +173,7 @@ } .discussion-form { - background-color: $white-light; -} - -.discussion-form-container { - padding: $gl-padding-top $gl-padding $gl-padding; + padding-top: $gl-padding-top; } .discussion-notes .disabled-comment { @@ -237,12 +233,7 @@ .discussion-body, .diff-file { .discussion-reply-holder { - background-color: $white-light; - padding: 10px 16px; - - &.is-replying { - padding-bottom: $gl-padding; - } + padding-top: $gl-padding; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 81e98f358a8..9d9cbecc958 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -47,7 +47,7 @@ ul.notes { } .timeline-entry-inner { - padding: $gl-padding $gl-btn-padding; + padding: $gl-padding 0; border-bottom: 1px solid $white-normal; } @@ -94,12 +94,6 @@ ul.notes { } } - &.note-discussion { - .timeline-entry-inner { - padding: $gl-padding 10px; - } - } - .editing-spinner { display: none; } @@ -352,6 +346,8 @@ ul.notes { } .discussion-notes { + background-color: $white-light; + &:not(:first-child) { border-top: 1px solid $white-normal; margin-top: 20px; @@ -363,10 +359,6 @@ ul.notes { } } - .notes { - background-color: $white-light; - } - a code { top: 0; margin-right: 0; @@ -647,8 +639,6 @@ ul.notes { border-bottom: 1px solid $white-normal; .timeline-entry-inner { - padding-left: $gl-padding; - padding-right: $gl-padding; border-bottom: 0; } } diff --git a/app/assets/stylesheets/pages/pages.scss b/app/assets/stylesheets/pages/pages.scss new file mode 100644 index 00000000000..fb42dee66d2 --- /dev/null +++ b/app/assets/stylesheets/pages/pages.scss @@ -0,0 +1,60 @@ +.pages-domain-list { + &-item { + position: relative; + display: flex; + align-items: center; + + .domain-status { + display: inline-flex; + left: $gl-padding; + position: absolute; + } + + .domain-name { + flex-grow: 1; + } + + } + + &.has-verification-status > li { + padding-left: 3 * $gl-padding; + } + +} + +.status-badge { + + display: inline-flex; + margin-bottom: $gl-padding-8; + + // Most of the following settings "stolen" from btn-sm + // Border radius is overwritten for both + .label, + .btn { + padding: $gl-padding-4 $gl-padding-8; + font-size: $gl-font-size; + line-height: $gl-btn-line-height; + border-radius: 0; + display: flex; + align-items: center; + } + + .btn svg { + top: auto; + } + + :first-child { + border-bottom-left-radius: $border-radius-default; + border-top-left-radius: $border-radius-default; + } + + :not(:first-child) { + border-left: 0; + } + + :last-child { + border-bottom-right-radius: $border-radius-default; + border-top-right-radius: $border-radius-default; + } + +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index ac745019319..b199f9876d3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -210,13 +210,8 @@ } .created-personal-access-token-container { - #created-personal-access-token { - width: 90%; - display: inline; - } - .btn-clipboard { - margin-left: 5px; + border: 1px solid $border-color; } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 1f6f7138e1f..8cc5c8fc877 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -308,14 +308,34 @@ height: 100%; } -.multi-file-editor-btn-group { - padding: $gl-bar-padding $gl-padding; - border-top: 1px solid $white-dark; +.preview-container { + height: 100%; + overflow: auto; + + .md-previewer { + padding: $gl-padding; + } +} + +.ide-mode-tabs { border-bottom: 1px solid $white-dark; - background: $white-light; + + .nav-links { + border-bottom: 0; + + li a { + padding: $gl-padding-8 $gl-padding; + line-height: $gl-btn-line-height; + } + } +} + +.ide-btn-group { + padding: $gl-padding-4 $gl-vert-padding; } .ide-status-bar { + border-top: 1px solid $white-dark; padding: $gl-bar-padding $gl-padding; background: $white-light; display: flex; diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index 7bc16214010..8e86af43fee 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -4,8 +4,8 @@ class Projects::DiscussionsController < Projects::ApplicationController before_action :check_merge_requests_available! before_action :merge_request - before_action :discussion - before_action :authorize_resolve_discussion! + before_action :discussion, only: [:resolve, :unresolve] + before_action :authorize_resolve_discussion!, only: [:resolve, :unresolve] def resolve Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion) diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index dd5e66f60e3..07249fe3182 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -7,6 +7,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController attr_reader :authentication_result, :redirected_path delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true + delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result alias_method :user, :actor alias_method :authenticated_user, :actor diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 45910a9be44..1dcf837f78e 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -64,7 +64,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, namespace_path: params[:namespace_id], project_path: project_path, - redirected_path: redirected_path) + redirected_path: redirected_path, auth_result_type: auth_result_type) end def access_actor diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 85e972d9731..dd12d30a085 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -2,7 +2,6 @@ class Projects::JobsController < Projects::ApplicationController include SendFileUpload before_action :build, except: [:index, :cancel_all] - before_action :authorize_read_build!, only: [:index, :show, :status, :raw, :trace] before_action :authorize_update_build!, @@ -45,8 +44,11 @@ class Projects::JobsController < Projects::ApplicationController end def show - @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC') - @builds = @builds.where("id not in (?)", @build.id) + @builds = @project.pipelines + .find_by_sha(@build.sha) + .builds + .order('id DESC') + .present(current_user: current_user) @pipeline = @build.pipeline respond_to do |format| @@ -128,7 +130,7 @@ class Projects::JobsController < Projects::ApplicationController if stream.file? send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline' else - render_404 + send_data stream.raw, type: 'text/plain; charset=utf-8', disposition: 'inline', filename: 'job.log' end end end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 2515e4b9a17..ebde0df1f7b 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -31,7 +31,9 @@ class Projects::LfsStorageController < Projects::GitHttpClientController render plain: 'Unprocessable entity', status: 422 end rescue ActiveRecord::RecordInvalid - render_400 + render_lfs_forbidden + rescue UploadedFile::InvalidPathError + render_lfs_forbidden rescue ObjectStorage::RemoteStoreError render_lfs_forbidden end @@ -66,10 +68,11 @@ class Projects::LfsStorageController < Projects::GitHttpClientController end def create_file!(oid, size) - LfsObject.new(oid: oid, size: size).tap do |object| - object.file.store_workhorse_file!(params, :file) - object.save! - end + uploaded_file = UploadedFile.from_params( + params, :file, LfsObjectUploader.workhorse_local_upload_path) + return unless uploaded_file + + LfsObject.create!(oid: oid, size: size, file: uploaded_file) end def link_to_project!(object) diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 557671ab186..73c613b26f3 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -4,41 +4,4 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController def show redirect_to project_settings_ci_cd_path(@project, params: params) end - - def update - Projects::UpdateService.new(project, current_user, update_params).tap do |service| - if service.execute - flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." - - run_autodevops_pipeline(service) - - redirect_to project_settings_ci_cd_path(@project) - else - render 'show' - end - end - end - - private - - def run_autodevops_pipeline(service) - return unless service.run_auto_devops_pipeline? - - if @project.empty_repo? - flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch." - return - end - - CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false) - flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe - end - - def update_params - params.require(:project).permit( - :runners_token, :builds_enabled, :build_allow_git_fetch, - :build_timeout_in_minutes, :build_coverage_regex, :public_builds, - :auto_cancel_pending_pipelines, :ci_config_path, - auto_devops_attributes: [:id, :domain, :enabled] - ) - end end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 2376f469213..48a09e1ddb8 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -25,7 +25,7 @@ class Projects::RefsController < Projects::ApplicationController when "graphs_commits" commits_project_graph_path(@project, @id) when "badges" - project_pipelines_settings_path(@project, ref: @id) + project_settings_ci_cd_path(@project, ref: @id) else project_commits_path(@project, @id) end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 96125b549b7..d80ef8113aa 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -2,13 +2,24 @@ module Projects module Settings class CiCdController < Projects::ApplicationController before_action :authorize_admin_pipeline! + before_action :define_variables def show - define_runners_variables - define_secret_variables - define_triggers_variables - define_badges_variables - define_auto_devops_variables + end + + def update + Projects::UpdateService.new(project, current_user, update_params).tap do |service| + result = service.execute + if result[:status] == :success + flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." + + run_autodevops_pipeline(service) + + redirect_to project_settings_ci_cd_path(@project) + else + render 'show' + end + end end def reset_cache @@ -25,6 +36,35 @@ module Projects private + def update_params + params.require(:project).permit( + :runners_token, :builds_enabled, :build_allow_git_fetch, + :build_timeout_human_readable, :build_coverage_regex, :public_builds, + :auto_cancel_pending_pipelines, :ci_config_path, + auto_devops_attributes: [:id, :domain, :enabled] + ) + end + + def run_autodevops_pipeline(service) + return unless service.run_auto_devops_pipeline? + + if @project.empty_repo? + flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch." + return + end + + CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false) + flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe + end + + def define_variables + define_runners_variables + define_secret_variables + define_triggers_variables + define_badges_variables + define_auto_devops_variables + end + def define_runners_variables @project_runners = @project.runners.ordered @assignable_runners = current_user.ci_authorized_runners diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ee197c75764..37f14230196 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -324,7 +324,7 @@ class ProjectsController < Projects::ApplicationController :avatar, :build_allow_git_fetch, :build_coverage_regex, - :build_timeout_in_minutes, + :build_timeout_human_readable, :resolve_outdated_diff_discussions, :container_registry_enabled, :default_branch, diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 2a6406d63c7..fb66dd0b766 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -16,7 +16,7 @@ class Appearance < ActiveRecord::Base has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - CACHE_KEY = 'current_appearance'.freeze + CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze after_commit :flush_redis_cache diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index df57b4f65e3..fbb95fe16df 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -7,6 +7,7 @@ module Ci belongs_to :project belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id + before_save :update_file_store before_save :set_size, if: :file_changed? scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } @@ -21,6 +22,10 @@ module Ci trace: 3 } + def update_file_store + self.file_store = file.object_store + end + def self.artifacts_size_for(project) self.where(project: project).sum(:size) end diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb index fa1eafb1d7a..593a9b3d71d 100644 --- a/app/models/concerns/chronic_duration_attribute.rb +++ b/app/models/concerns/chronic_duration_attribute.rb @@ -8,14 +8,14 @@ module ChronicDurationAttribute end end - def chronic_duration_attr_writer(virtual_attribute, source_attribute) + def chronic_duration_attr_writer(virtual_attribute, source_attribute, parameters = {}) chronic_duration_attr_reader(virtual_attribute, source_attribute) define_method("#{virtual_attribute}=") do |value| - chronic_duration_attributes[virtual_attribute] = value.presence || '' + chronic_duration_attributes[virtual_attribute] = value.presence || parameters[:default].presence.to_s begin - new_value = ChronicDuration.parse(value).to_i if value.present? + new_value = value.present? ? ChronicDuration.parse(value).to_i : parameters[:default].presence assign_attributes(source_attribute => new_value) rescue ChronicDuration::DurationParseError # ignore error as it will be caught by validation diff --git a/app/models/concerns/presentable.rb b/app/models/concerns/presentable.rb index 7b33b837004..bc4fbd19a02 100644 --- a/app/models/concerns/presentable.rb +++ b/app/models/concerns/presentable.rb @@ -1,4 +1,12 @@ module Presentable + extend ActiveSupport::Concern + + class_methods do + def present(attributes) + all.map { |klass_object| klass_object.present(attributes) } + end + end + def present(**attributes) Gitlab::View::Presenter::Factory .new(self, attributes) diff --git a/app/models/note.rb b/app/models/note.rb index 109405d3f17..e426f84832b 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -317,6 +317,10 @@ class Note < ActiveRecord::Base !system? && !for_snippet? end + def can_create_notification? + true + end + def discussion_class(noteable = nil) # When commit notes are rendered on an MR's Discussion page, they are # displayed in one discussion instead of individually. diff --git a/app/models/project.rb b/app/models/project.rb index 32289106f28..1b29cbf28d2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -21,6 +21,7 @@ class Project < ActiveRecord::Base include Gitlab::SQL::Pattern include DeploymentPlatform include ::Gitlab::Utils::StrongMemoize + include ChronicDurationAttribute extend Gitlab::ConfigHelper @@ -325,6 +326,12 @@ class Project < ActiveRecord::Base enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } + chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600 + + validates :build_timeout, allow_nil: true, + numericality: { greater_than_or_equal_to: 600, + message: 'needs to be at least 10 minutes' } + # Returns a collection of projects that is either public or visible to the # logged in user. def self.public_or_visible_to_user(user = nil) @@ -630,7 +637,7 @@ class Project < ActiveRecord::Base end def create_or_update_import_data(data: nil, credentials: nil) - return unless import_url.present? && valid_import_url? + return if data.nil? && credentials.nil? project_import_data = import_data || build_import_data if data @@ -1309,14 +1316,6 @@ class Project < ActiveRecord::Base self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end - def build_timeout_in_minutes - build_timeout / 60 - end - - def build_timeout_in_minutes=(value) - self.build_timeout = value.to_i * 60 - end - def open_issues_count Projects::OpenIssuesCountService.new(self).count end @@ -1488,6 +1487,7 @@ class Project < ActiveRecord::Base remove_import_jid update_project_counter_caches after_create_default_branch + refresh_markdown_cache! end def update_project_counter_caches diff --git a/app/models/user.rb b/app/models/user.rb index ba51595e6a3..7b6857a0d34 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -164,12 +164,15 @@ class User < ActiveRecord::Base before_validation :sanitize_attrs before_validation :set_notification_email, if: :email_changed? + before_save :set_notification_email, if: :email_changed? # in case validation is skipped before_validation :set_public_email, if: :public_email_changed? + before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } before_validation :ensure_namespace_correct + before_save :ensure_namespace_correct # in case validation is skipped after_validation :set_username_errors after_update :username_changed_hook, if: :username_changed? after_destroy :post_destroy_hook @@ -408,7 +411,6 @@ class User < ActiveRecord::Base unique_internal(where(ghost: true), 'ghost', email) do |u| u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.' u.name = 'Ghost User' - u.notification_email = email end end end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index 3f6d7d04667..e86d1c8f98e 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -2,20 +2,6 @@ class IssuablePolicy < BasePolicy delegate { @subject.project } condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? } - - # We aren't checking `:read_issue` or `:read_merge_request` in this case - # because it could be possible for a user to see an issuable-iid - # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be allowed - # to read the actual issue after a more expensive `:read_issue` check. - # - # `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee. - condition(:visible_to_user, score: 4) do - Project.where(id: @subject.project) - .public_or_visible_to_user(@user) - .with_feature_available_for_user(@subject, @user) - .any? - end - condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) } desc "User is the assignee or author" diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index ed499511999..263c6e3039c 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -17,6 +17,4 @@ class IssuePolicy < IssuablePolicy prevent :update_issue prevent :admin_issue end - - rule { can?(:read_issue) | visible_to_user }.enable :read_issue_iid end diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb index e003376d219..c3fe857f8a2 100644 --- a/app/policies/merge_request_policy.rb +++ b/app/policies/merge_request_policy.rb @@ -1,3 +1,2 @@ class MergeRequestPolicy < IssuablePolicy - rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 57ab0c23dcd..b1ed034cd00 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -66,6 +66,22 @@ class ProjectPolicy < BasePolicy project.merge_requests_allowing_push_to_user(user).any? end + # We aren't checking `:read_issue` or `:read_merge_request` in this case + # because it could be possible for a user to see an issuable-iid + # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be + # allowed to read the actual issue after a more expensive `:read_issue` + # check. These checks are intended to be used alongside + # `:read_project_for_iids`. + # + # `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee. + condition(:issues_visible_to_user, score: 4) do + @subject.feature_available?(:issues, @user) + end + + condition(:merge_requests_visible_to_user, score: 4) do + @subject.feature_available?(:merge_requests, @user) + end + features = %w[ merge_requests issues @@ -81,6 +97,10 @@ class ProjectPolicy < BasePolicy condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) } end + # `:read_project` may be prevented in EE, but `:read_project_for_iids` should + # not. + rule { guest | admin }.enable :read_project_for_iids + rule { guest }.enable :guest_access rule { reporter }.enable :reporter_access rule { developer }.enable :developer_access @@ -150,6 +170,7 @@ class ProjectPolicy < BasePolicy # where we enable or prevent it based on other coditions. rule { (~anonymous & public_project) | internal_access }.policy do enable :public_user_access + enable :read_project_for_iids end rule { can?(:public_user_access) }.policy do @@ -255,7 +276,11 @@ class ProjectPolicy < BasePolicy end rule { anonymous & ~public_project }.prevent_all - rule { public_project }.enable(:public_access) + + rule { public_project }.policy do + enable :public_access + enable :read_project_for_iids + end rule { can?(:public_access) }.policy do enable :read_project @@ -305,6 +330,14 @@ class ProjectPolicy < BasePolicy enable :update_pipeline end + rule do + (can?(:read_project_for_iids) & issues_visible_to_user) | can?(:read_issue) + end.enable :read_issue_iid + + rule do + (can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request) + end.enable :read_merge_request_iid + private def team_member? diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb index 255475e1fe6..9afebda19be 100644 --- a/app/presenters/ci/build_presenter.rb +++ b/app/presenters/ci/build_presenter.rb @@ -15,6 +15,8 @@ module Ci def status_title if auto_canceled? "Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" + else + tooltip_for_badge end end @@ -28,5 +30,19 @@ module Ci trigger_request.user_variables end end + + def tooltip_message + "#{subject.name} - #{detailed_status.status_tooltip}" + end + + private + + def tooltip_for_badge + detailed_status.badge_tooltip.capitalize + end + + def detailed_status + @detailed_status ||= subject.detailed_status(user) + end end end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index a7c2e21e92b..8e8bda2f9df 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -2,7 +2,7 @@ class StatusEntity < Grape::Entity include RequestAwareEntity expose :icon, :text, :label, :group - + expose :status_tooltip, as: :tooltip expose :has_details?, as: :has_details expose :details_path diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index e09b445636f..d46dcff34a1 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -4,6 +4,9 @@ module Ci class RegisterJobService attr_reader :runner + JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze + JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze + Result = Struct.new(:build, :valid?) def initialize(runner) @@ -104,10 +107,22 @@ module Ci end def register_success(job) - job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at) + labels = { shared_runner: runner.shared?, + jobs_running_for_project: jobs_running_for_project(job) } + + job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) attempt_counter.increment end + def jobs_running_for_project(job) + return '+Inf' unless runner.shared? + + # excluding currently started job + running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared) + .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1 + running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+" + end + def failed_attempt_counter @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job") end @@ -117,7 +132,7 @@ module Ci end def job_queue_duration_seconds - @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time') + @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS) end end end diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index f0cab2ade6d..199b8028dbc 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -11,7 +11,7 @@ module Notes unless @note.system? EventCreateService.new.leave_note(@note, @note.author) - return unless @note.for_project_noteable? + return if @note.for_personal_snippet? @note.create_cross_references! execute_note_hooks @@ -23,6 +23,8 @@ module Notes end def execute_note_hooks + return unless @note.project + note_data = hook_data hooks_scope = @note.confidential? ? :confidential_note_hooks : :note_hooks diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb index a68ecb4abe1..fb4afb85588 100644 --- a/app/services/projects/gitlab_projects_import_service.rb +++ b/app/services/projects/gitlab_projects_import_service.rb @@ -5,8 +5,8 @@ module Projects class GitlabProjectsImportService attr_reader :current_user, :params - def initialize(user, params) - @current_user, @params = user, params.dup + def initialize(user, import_params, override_params = nil) + @current_user, @params, @override_params = user, import_params.dup, override_params end def execute @@ -17,6 +17,7 @@ module Projects params[:import_type] = 'gitlab_project' params[:import_source] = import_upload_path + params[:import_data] = { data: { override_params: @override_params } } if @override_params ::Projects::CreateService.new(current_user, params).execute end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 2253d638e93..00bf5434b7f 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -429,7 +429,7 @@ module SystemNoteService def cross_reference(noteable, mentioner, author) return if cross_reference_disallowed?(noteable, mentioner) - gfm_reference = mentioner.gfm_reference(noteable.project) + gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group) body = cross_reference_note_content(gfm_reference) if noteable.is_a?(ExternalIssue) @@ -582,7 +582,7 @@ module SystemNoteService text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}" notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) else - gfm_reference = mentioner.gfm_reference(noteable.project) + gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group) text = cross_reference_note_content(gfm_reference) notes.where(note: [text, text.capitalize]) end diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb index ef0f8acefd6..dd86753479d 100644 --- a/app/uploaders/job_artifact_uploader.rb +++ b/app/uploaders/job_artifact_uploader.rb @@ -2,6 +2,8 @@ class JobArtifactUploader < GitlabUploader extend Workhorse::UploadPath include ObjectStorage::Concern + ObjectNotReadyError = Class.new(StandardError) + storage_options Gitlab.config.artifacts def size @@ -25,6 +27,8 @@ class JobArtifactUploader < GitlabUploader private def dynamic_segment + raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id + creation_date = model.created_at.utc.strftime('%Y_%m_%d') File.join(disk_hash[0..1], disk_hash[2..3], disk_hash, diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb index b726b053493..efb7893d153 100644 --- a/app/uploaders/legacy_artifact_uploader.rb +++ b/app/uploaders/legacy_artifact_uploader.rb @@ -2,6 +2,8 @@ class LegacyArtifactUploader < GitlabUploader extend Workhorse::UploadPath include ObjectStorage::Concern + ObjectNotReadyError = Class.new(StandardError) + storage_options Gitlab.config.artifacts def store_dir @@ -11,6 +13,8 @@ class LegacyArtifactUploader < GitlabUploader private def dynamic_segment + raise ObjectNotReadyError, 'Build is not ready' unless model.id + File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s) end end diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 4028b052768..bd258e04d3f 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -128,7 +128,7 @@ module ObjectStorage end def direct_upload_enabled? - object_store_options.direct_upload + object_store_options&.direct_upload end def background_upload_enabled? @@ -156,11 +156,10 @@ module ObjectStorage end def workhorse_authorize - if options = workhorse_remote_upload_options - { RemoteObject: options } - else - { TempPath: workhorse_local_upload_path } - end + { + RemoteObject: workhorse_remote_upload_options, + TempPath: workhorse_local_upload_path + }.compact end def workhorse_local_upload_path @@ -184,6 +183,14 @@ module ObjectStorage StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options) } end + + def default_object_store + if self.object_store_enabled? && self.direct_upload_enabled? + Store::REMOTE + else + Store::LOCAL + end + end end # allow to configure and overwrite the filename @@ -204,12 +211,12 @@ module ObjectStorage end def object_store - @object_store ||= model.try(store_serialization_column) || Store::LOCAL + @object_store ||= model.try(store_serialization_column) || self.class.default_object_store end # rubocop:disable Gitlab/ModuleWithInstanceVariables def object_store=(value) - @object_store = value || Store::LOCAL + @object_store = value || self.class.default_object_store @storage = storage_for(object_store) end # rubocop:enable Gitlab/ModuleWithInstanceVariables @@ -285,16 +292,14 @@ module ObjectStorage } end - def store_workhorse_file!(params, identifier) - filename = params["#{identifier}.name"] - - if remote_object_id = params["#{identifier}.remote_id"] - store_remote_file!(remote_object_id, filename) - elsif local_path = params["#{identifier}.path"] - store_local_file!(local_path, filename) - else - raise RemoteStoreError, 'Bad file' + def cache!(new_file = sanitized_file) + # We intercept ::UploadedFile which might be stored on remote storage + # We use that for "accelerated" uploads, where we store result on remote storage + if new_file.is_a?(::UploadedFile) && new_file.remote_id + return cache_remote_file!(new_file.remote_id, new_file.original_filename) end + + super end private @@ -305,36 +310,29 @@ module ObjectStorage self.file_storage? end - def store_remote_file!(remote_object_id, filename) - raise RemoteStoreError, 'Missing filename' unless filename - + def cache_remote_file!(remote_object_id, original_filename) file_path = File.join(TMP_UPLOAD_PATH, remote_object_id) file_path = Pathname.new(file_path).cleanpath.to_s raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(TMP_UPLOAD_PATH + '/') - self.object_store = Store::REMOTE - # TODO: # This should be changed to make use of `tmp/cache` mechanism # instead of using custom upload directory, # using tmp/cache makes this implementation way easier than it is today - CarrierWave::Storage::Fog::File.new(self, storage, file_path).tap do |file| + CarrierWave::Storage::Fog::File.new(self, storage_for(Store::REMOTE), file_path).tap do |file| raise RemoteStoreError, 'Missing file' unless file.exists? - self.filename = filename - self.file = storage.store!(file) - end - end - - def store_local_file!(local_path, filename) - raise RemoteStoreError, 'Missing filename' unless filename + # Remote stored file, we force to store on remote storage + self.object_store = Store::REMOTE - root_path = File.realpath(self.class.workhorse_local_upload_path) - file_path = File.realpath(local_path) - raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(root_path) - - self.object_store = Store::LOCAL - self.store!(UploadedFile.new(file_path, filename)) + # TODO: + # We store file internally and force it to be considered as `cached` + # This makes CarrierWave to store file in permament location (copy/delete) + # once this object is saved, but not sooner + @cache_id = "force-to-use-cache" # rubocop:disable Gitlab/ModuleWithInstanceVariables + @file = file # rubocop:disable Gitlab/ModuleWithInstanceVariables + @filename = original_filename # rubocop:disable Gitlab/ModuleWithInstanceVariables + end end # this is a hack around CarrierWave. The #migrate method needs to be diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml index 35a3563dff1..5114387984b 100644 --- a/app/views/ci/status/_badge.html.haml +++ b/app/views/ci/status/_badge.html.haml @@ -4,10 +4,10 @@ - css_classes = "ci-status ci-#{status.group} #{'has-tooltip' if title.present?}" - if link && status.has_details? - = link_to status.details_path, class: css_classes, title: title do + = link_to status.details_path, class: css_classes, title: title, data: { html: title.present? } do = sprite_icon(status.icon) = status.text - else - %span{ class: css_classes, title: title } + %span{ class: css_classes, title: title, data: { html: title.present? } } = sprite_icon(status.icon) = status.text diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index c5b4439e273..db2040110fa 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -3,14 +3,15 @@ - subject = local_assigns.fetch(:subject) - status = subject.detailed_status(current_user) - klass = "ci-status-icon ci-status-icon-#{status.group}" -- tooltip = "#{subject.name} - #{status.label}" +- tooltip = "#{subject.name} - #{status.status_tooltip}" - if status.has_details? - = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: true, container: 'body' } do %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name + - else - .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } + .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' } } %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 78848542810..b96251cd982 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -19,8 +19,10 @@ %h5.prepend-top-0 Your New Personal Access Token .form-group - = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block" - = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left") + .input-group + = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block" + %span.input-group-btn + = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left", class: "btn-default btn-clipboard") %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. %hr diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index ecf186e3dc8..0b57ebedebd 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -1,5 +1,3 @@ -- builds = @build.pipeline.builds.to_a - %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } .sidebar-container .blocks-container @@ -91,7 +89,8 @@ - HasStatus::ORDERED_STATUSES.each do |build_status| - builds.select{|build| build.status == build_status}.each do |build| .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } - = link_to project_job_path(@project, build) do + - tooltip = build.tooltip_message + = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' }) do = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right') %span{ class: "ci-status-icon-#{build.status}" } = ci_icon_for_status(build.status) @@ -101,5 +100,4 @@ - else = build.id - if build.retried? - %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } - = sprite_icon('retry', size:16, css_class: 'icon-retry') + = sprite_icon('retry', size:16, css_class: 'icon-retry') diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index fa27ded7cc2..dece4dfe167 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -107,7 +107,7 @@ illustration_size: 'svg-430', title: _('This job has not started yet'), content: _('This job is in pending state and is waiting to be picked by a runner') - = render "sidebar" + = render "sidebar", builds: @builds .js-build-options{ data: javascript_build_options } diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index 75df92b05a7..27bbe52a714 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -1,28 +1,29 @@ +- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? + - if can?(current_user, :update_pages, @project) && @domains.any? .panel.panel-default .panel-heading Domains (#{@domains.count}) - %ul.well-list - - verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? + %ul.well-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) } - @domains.each do |domain| - %li - .pull-right + %li.pages-domain-list-item.unstyled + - if verification_enabled + - tooltip, status = domain.unverified? ? [_('Unverified'), 'failed'] : [_('Verified'), 'success'] + .domain-status.ci-status-icon.has-tooltip{ class: "ci-status-icon-#{status}", title: tooltip } + = sprite_icon("status_#{status}", size: 16 ) + .domain-name + = link_to domain.url do + = domain.url + = icon('external-link') + - if domain.subject + %p + %span.label.label-gray Certificate: #{domain.subject} + - if domain.expired? + %span.label.label-danger Expired + %div = link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped" = link_to 'Remove', project_pages_domain_path(@project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" - .clearfix - - if verification_enabled - - tooltip, status = domain.unverified? ? ['Unverified', 'failed'] : ['Verified', 'success'] - = link_to domain.url, title: tooltip, class: 'has-tooltip' do - = sprite_icon("status_#{status}", size: 16, css_class: "has-tooltip ci-status-icon ci-status-icon-#{status}") - = domain.domain - - else - = link_to domain.domain, domain.url - %p - - if domain.subject - %span.label.label-gray Certificate: #{domain.subject} - - if domain.expired? - %span.label.label-danger Expired - if verification_enabled && domain.unverified? %li.warning-row #{domain.domain} is not verified. To learn how to verify ownership, visit your - = link_to 'domain details', project_pages_domain_path(@project, domain) + #{link_to 'domain details', project_pages_domain_path(@project, domain)}. diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index f17d9d24db6..6adaea799b2 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -1,11 +1,10 @@ - page_title 'Pages' -%h3.page_title +%h3.page-title.with-button Pages - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https) = link_to new_project_pages_domain_path(@project), class: 'btn btn-new pull-right', title: 'New Domain' do - %i.fa.fa-plus New Domain %p.light diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml index 5645a4604bf..6c404990492 100644 --- a/app/views/projects/pages_domains/edit.html.haml +++ b/app/views/projects/pages_domains/edit.html.haml @@ -1,7 +1,7 @@ - add_to_breadcrumbs "Pages", project_pages_path(@project) - breadcrumb_title @domain.domain - page_title @domain.domain -%h3.page_title +%h3.page-title = @domain.domain %hr.clearfix %div diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml index e49163880c7..269df803a2b 100644 --- a/app/views/projects/pages_domains/new.html.haml +++ b/app/views/projects/pages_domains/new.html.haml @@ -1,6 +1,6 @@ - add_to_breadcrumbs "Pages", project_pages_path(@project) - page_title 'New Pages Domain' -%h3.page_title +%h3.page-title New Pages Domain %hr.clearfix %div diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index ba0713daee9..44d66f3b2d0 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -1,17 +1,19 @@ - add_to_breadcrumbs "Pages", project_pages_path(@project) - breadcrumb_title @domain.domain - page_title "#{@domain.domain}", 'Pages Domains' +- dns_record = "#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}." - verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? + - if verification_enabled && @domain.unverified? - %p.alert.alert-warning - %strong - This domain is not verified. You will need to verify ownership before - access is enabled. + = content_for :flash_message do + .alert.alert-warning + .container-fluid.container-limited + This domain is not verified. You will need to verify ownership before access is enabled. -%h3.page-title - Pages Domain +%h3.page-title.with-button = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right' + Pages Domain .table-holder %table.table @@ -19,31 +21,41 @@ %td Domain %td - = link_to @domain.domain, @domain.url + = link_to @domain.url do + = @domain.url + = icon('external-link') %tr %td DNS %td - %p - To access this domain create a new DNS record: - %pre - #{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}. + .input-group + = text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true + .input-group-btn + = clipboard_button(target: '#domain_dns', class: 'btn-default hidden-xs') + %p.help-block + To access this domain create a new DNS record + - if verification_enabled + - verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}" %tr %td Verification status %td - %p + = form_tag verify_project_pages_domain_path(@project, @domain) do + .status-badge + - text, status = @domain.unverified? ? [_('Unverified'), 'label-danger'] : [_('Verified'), 'label-success'] + .label{ class: status } + = text + %button.btn.has-tooltip{ type: "submit", data: { container: 'body' }, title: _("Retry verification") } + = sprite_icon('redo') + .input-group + = text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true + .input-group-btn + = clipboard_button(target: '#domain_verification', class: 'btn-default hidden-xs') + %p.help-block - help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') - To #{link_to 'verify ownership', help_link} of your domain, create - this DNS record: - %pre - #{@domain.verification_domain} TXT #{@domain.keyed_verification_code} - %p - - if @domain.verified? - #{@domain.domain} has been successfully verified. - - else - = button_to 'Verify ownership', verify_project_pages_domain_path(@project, @domain), class: 'btn btn-save btn-sm' + To #{link_to 'verify ownership', help_link} of your domain, + add the above key to a TXT record within to your DNS configuration. %tr %td diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml index e8028059487..e8028059487 100644 --- a/app/views/projects/pipelines_settings/_badge.html.haml +++ b/app/views/projects/settings/ci_cd/_badge.html.haml diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 646c01c0989..20868f9ba5d 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -1,6 +1,7 @@ .row.prepend-top-default .col-lg-12 - = form_for @project, url: project_pipelines_settings_path(@project) do |f| + = form_for @project, url: project_settings_ci_cd_path(@project) do |f| + = form_errors(@project) %fieldset.builds-feature .form-group %h5 Auto DevOps (Beta) @@ -73,10 +74,10 @@ %hr .form-group - = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light' - = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' + = f.label :build_timeout_human_readable, 'Timeout', class: 'label-light' + = f.text_field :build_timeout_human_readable, class: 'form-control' %p.help-block - Per job in minutes. If a job passes this threshold, it will be marked as failed + Per job. If a job passes this threshold, it will be marked as failed = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' %hr @@ -151,10 +152,13 @@ %li excoveralls (Elixir) - %code \[TOTAL\]\s+(\d+\.\d+)% + %li + JaCoCo (Java/Kotlin) + %code Total.*?([0-9]{1,3})% = f.submit 'Save changes', class: "btn btn-save" %hr .row.prepend-top-default - = render partial: 'projects/pipelines_settings/badge', collection: @badges + = render partial: 'badge', collection: @badges diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index d65341dbd40..09268c9943b 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -3,8 +3,9 @@ - page_title "CI / CD" - expanded = Rails.env.test? +- general_expanded = @project.errors.empty? ? expanded : true -%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) } +%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } .settings-header %h4 General pipelines settings @@ -13,7 +14,7 @@ %p Update your CI/CD configuration, like job timeout or Auto DevOps. .settings-content - = render 'projects/pipelines_settings/show' + = render 'form' %section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 71c0d740bc8..725bf916592 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -24,21 +24,20 @@ -# DiffNote = f.hidden_field :position - .discussion-form-container - = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do - = render 'projects/zen', f: f, - attr: :note, - classes: 'note-textarea js-note-text', - placeholder: "Write a comment or drag your files here...", - supports_quick_actions: supports_quick_actions, - supports_autocomplete: supports_autocomplete - = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions - .error-alert - - .note-form-actions.clearfix - = render partial: 'shared/notes/comment_button' - - = yield(:note_actions) - - %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } - Discard draft + = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do + = render 'projects/zen', f: f, + attr: :note, + classes: 'note-textarea js-note-text', + placeholder: "Write a comment or drag your files here...", + supports_quick_actions: supports_quick_actions, + supports_autocomplete: supports_autocomplete + = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions + .error-alert + + .note-form-actions.clearfix + = render partial: 'shared/notes/comment_button' + + = yield(:note_actions) + + %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } + Discard draft diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb index 67c54fbf10e..b925741934a 100644 --- a/app/workers/new_note_worker.rb +++ b/app/workers/new_note_worker.rb @@ -5,7 +5,7 @@ class NewNoteWorker # old `NewNoteWorker` jobs (can remove later) def perform(note_id, _params = {}) if note = Note.find_by(id: note_id) - NotificationService.new.new_note(note) + NotificationService.new.new_note(note) if note.can_create_notification? Notes::PostProcessService.new(note).execute else Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job") |