diff options
Diffstat (limited to 'app')
101 files changed, 1385 insertions, 438 deletions
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index b4bfaee1d85..155c348286c 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -93,7 +93,7 @@ export default { <icon :size="16" class="prepend-left-8 append-right-8" - name="doc_image" + name="doc-image" aria-hidden="true" /> </div> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 200d1923635..bc263cbbfea 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,5 +1,3 @@ -/* eslint-disable quote-props, comma-dangle */ - import $ from 'jquery'; import _ from 'underscore'; import Vue from 'vue'; @@ -47,7 +45,7 @@ export default () => { gl.IssueBoardsApp = new Vue({ el: $boardApp, components: { - 'board': gl.issueBoards.Board, + board: gl.issueBoards.Board, 'board-sidebar': gl.issueBoards.BoardSidebar, BoardAddIssuesModal, }, @@ -65,11 +63,11 @@ export default () => { defaultAvatar: $boardApp.dataset.defaultAvatar, }, computed: { - detailIssueVisible () { + detailIssueVisible() { return Object.keys(this.detailIssue.issue).length; }, }, - created () { + created() { gl.boardService = new BoardService({ boardsEndpoint: this.boardsEndpoint, listsEndpoint: this.listsEndpoint, @@ -89,15 +87,16 @@ export default () => { eventHub.$off('clearDetailIssue', this.clearDetailIssue); sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); }, - mounted () { + mounted() { this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit); this.filterManager.setup(); Store.disabled = this.disabled; - gl.boardService.all() + gl.boardService + .all() .then(res => res.data) - .then((data) => { - data.forEach((board) => { + .then(data => { + data.forEach(board => { const list = Store.addList(board, this.defaultAvatar); if (list.type === 'closed') { @@ -126,7 +125,7 @@ export default () => { newIssue.setFetchingState('subscriptions', true); BoardService.getIssueInfo(sidebarInfoEndpoint) .then(res => res.data) - .then((data) => { + .then(data => { newIssue.setFetchingState('subscriptions', false); newIssue.updateData({ subscribed: data.subscribed, @@ -159,7 +158,7 @@ export default () => { Flash(__('An error occurred when toggling the notification subscription')); }); } - } + }, }, }); @@ -168,77 +167,81 @@ export default () => { data: { filters: Store.state.filters, }, - mounted () { + mounted() { gl.issueBoards.newListDropdownInit(); }, }); - gl.IssueBoardsModalAddBtn = new Vue({ - el: document.getElementById('js-add-issues-btn'), - mixins: [modalMixin], - data() { - return { - modal: ModalStore.store, - store: Store.state, - canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), - }; - }, - computed: { - disabled() { - if (!this.store) { - return true; - } - return !this.store.lists.filter(list => !list.preset).length; + const issueBoardsModal = document.getElementById('js-add-issues-btn'); + + if (issueBoardsModal) { + gl.IssueBoardsModalAddBtn = new Vue({ + el: issueBoardsModal, + mixins: [modalMixin], + data() { + return { + modal: ModalStore.store, + store: Store.state, + canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), + }; }, - tooltipTitle() { - if (this.disabled) { - return 'Please add a list to your board first'; - } + computed: { + disabled() { + if (!this.store) { + return true; + } + return !this.store.lists.filter(list => !list.preset).length; + }, + tooltipTitle() { + if (this.disabled) { + return 'Please add a list to your board first'; + } - return ''; + return ''; + }, }, - }, - watch: { - disabled() { + watch: { + disabled() { + this.updateTooltip(); + }, + }, + mounted() { this.updateTooltip(); }, - }, - mounted() { - this.updateTooltip(); - }, - methods: { - updateTooltip() { - const $tooltip = $(this.$refs.addIssuesButton); - - this.$nextTick(() => { - if (this.disabled) { - $tooltip.tooltip(); - } else { - $tooltip.tooltip('dispose'); + methods: { + updateTooltip() { + const $tooltip = $(this.$refs.addIssuesButton); + + this.$nextTick(() => { + if (this.disabled) { + $tooltip.tooltip(); + } else { + $tooltip.tooltip('dispose'); + } + }); + }, + openModal() { + if (!this.disabled) { + this.toggleModal(true); } - }); - }, - openModal() { - if (!this.disabled) { - this.toggleModal(true); - } + }, }, - }, - template: ` - <div class="board-extra-actions"> - <button - class="btn btn-create prepend-left-10" - type="button" - data-placement="bottom" - ref="addIssuesButton" - :class="{ 'disabled': disabled }" - :title="tooltipTitle" - :aria-disabled="disabled" - v-if="canAdminList" - @click="openModal"> - Add issues - </button> - </div> - `, - }); + template: ` + <div class="board-extra-actions"> + <button + class="btn btn-create prepend-left-10" + type="button" + data-placement="bottom" + ref="addIssuesButton" + :class="{ 'disabled': disabled }" + :title="tooltipTitle" + :aria-disabled="disabled" + v-if="canAdminList" + @click="openModal"> + Add issues + </button> + </div> + `, + }); + } }; diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js new file mode 100644 index 00000000000..923c036f5a4 --- /dev/null +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -0,0 +1,4 @@ +import Vue from 'vue'; +import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar'; + +Vue.component('gl-progress-bar', progressBar); diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 0d2fe2925d8..ea945cd3fa5 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -3,4 +3,5 @@ import './polyfills'; import './jquery'; import './bootstrap'; import './vue'; +import './gitlab_ui'; import '../lib/utils/axios_utils'; diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue index a4e06bbbe3c..720ae11aaa6 100644 --- a/app/assets/javascripts/ide/components/changed_file_icon.vue +++ b/app/assets/javascripts/ide/components/changed_file_icon.vue @@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; +import { getCommitIconMap } from '../utils'; export default { components: { @@ -34,16 +35,14 @@ export default { }, computed: { changedIcon() { - const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : ''; - return this.file.tempFile && !this.forceModifiedIcon - ? `file-addition${suffix}` - : `file-modified${suffix}`; - }, - stagedIcon() { - return `${this.changedIcon}-solid`; + const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : ''; + + if (this.forceModifiedIcon) return `file-modified${suffix}`; + + return `${getCommitIconMap(this.file).icon}${suffix}`; }, changedIconClass() { - return `multi-${this.changedIcon} float-left`; + return `ide-${this.changedIcon} float-left`; }, tooltipTitle() { if (!this.showTooltip) return undefined; @@ -66,6 +65,9 @@ export default { return undefined; }, + showIcon() { + return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted; + }, }, }; </script> @@ -79,7 +81,7 @@ export default { class="ide-file-changed-icon" > <icon - v-if="file.changed || file.tempFile || file.staged" + v-if="showIcon" :name="changedIcon" :size="12" :css-classes="changedIconClass" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index eb7cb9745ec..a8b5c7a16d0 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import { mapActions, mapState, mapGetters } from 'vuex'; import { sprintf, __ } from '~/locale'; import * as consts from '../../stores/modules/commit/constants'; @@ -14,7 +15,7 @@ export default { commitToCurrentBranchText() { return sprintf( __('Commit to %{branchName} branch'), - { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` }, + { branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` }, false, ); }, diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index ee21eeda3cd..391004dcd3c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; import { viewerTypes } from '../../constants'; +import { getCommitIconMap } from '../../utils'; export default { components: { @@ -42,11 +43,12 @@ export default { }, computed: { iconName() { - const prefix = this.stagedList ? '-solid' : ''; - return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`; + const suffix = this.stagedList ? '-solid' : ''; + + return `${getCommitIconMap(this.file).icon}${suffix}`; }, iconClass() { - return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; + return `${getCommitIconMap(this.file).class} append-right-8`; }, fullKey() { return `${this.keyPrefix}-${this.file.key}`; @@ -67,6 +69,8 @@ export default { 'stageChange', ]), openFileInEditor() { + if (this.file.type === 'tree') return null; + return this.openPendingTab({ file: this.file, keyPrefix: this.keyPrefix, diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index 7014b9f605e..e6044401c9f 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -56,7 +56,7 @@ export default { > <icon :size="12" - name="more" + name="ellipsis_h" /> </button> <div class="dropdown-menu dropdown-menu-right"> diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index f9978762c45..d09c99050fe 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -10,7 +10,7 @@ export default { EditorModeDropdown, }, computed: { - ...mapGetters(['currentMergeRequest']), + ...mapGetters(['currentMergeRequest', 'activeFile']), ...mapState(['viewer', 'currentMergeRequestId']), showLatestChangesText() { return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; @@ -23,12 +23,20 @@ export default { }, }, mounted() { + if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) { + this.$router.push(`/project${this.activeFile.url}`, () => { + this.updateViewer('editor'); + }); + } else if (this.activeFile && this.activeFile.deleted) { + this.resetOpenFiles(); + } + this.$nextTick(() => { this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); }); }, methods: { - ...mapActions(['updateViewer']), + ...mapActions(['updateViewer', 'resetOpenFiles']), }, }; </script> @@ -36,7 +44,6 @@ export default { <template> <ide-tree-list :viewer-type="viewer" - :disable-action-dropdown="true" header-class="ide-review-header" > <template diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 0a95c0bb30d..e996dd9059e 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -17,14 +17,18 @@ export default { ...mapGetters(['currentProject', 'currentTree', 'activeFile']), }, mounted() { - if (this.activeFile && this.activeFile.pending) { + if (!this.activeFile) return; + + if (this.activeFile.pending && !this.activeFile.deleted) { this.$router.push(`/project${this.activeFile.url}`, () => { this.updateViewer('editor'); }); + } else if (this.activeFile.deleted) { + this.resetOpenFiles(); } }, methods: { - ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']), + ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']), }, }; </script> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 0df99798d21..2e7226b727c 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -22,11 +22,6 @@ export default { required: false, default: null, }, - disableActionDropdown: { - type: Boolean, - required: false, - default: false, - }, }, computed: { ...mapState(['currentBranchId']), @@ -69,7 +64,6 @@ export default { :key="file.key" :file="file" :level="0" - :disable-action-dropdown="disableActionDropdown" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index c29e49ba766..440e480d596 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -13,7 +13,7 @@ export default { ItemButton, }, props: { - branch: { + type: { type: String, required: true, }, @@ -45,7 +45,7 @@ export default { }, }, methods: { - ...mapActions(['createTempEntry', 'openNewEntryModal']), + ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']), createNewItem(type) { this.openNewEntryModal({ type, path: this.path }); this.dropdownOpen = false; @@ -82,28 +82,40 @@ export default { ref="dropdownMenu" class="dropdown-menu dropdown-menu-right" > + <template v-if="type === 'tree'"> + <li> + <item-button + :label="__('New file')" + class="d-flex" + icon="doc-new" + icon-classes="mr-2" + @click="createNewItem('blob')" + /> + </li> + <li> + <upload + :path="path" + @create="createTempEntry" + /> + </li> + <li> + <item-button + :label="__('New directory')" + class="d-flex" + icon="folder-new" + icon-classes="mr-2" + @click="createNewItem('tree')" + /> + </li> + <li class="divider"></li> + </template> <li> <item-button - :label="__('New file')" + :label="__('Delete')" class="d-flex" - icon="doc-new" + icon="remove" icon-classes="mr-2" - @click="createNewItem('blob')" - /> - </li> - <li> - <upload - :path="path" - @create="createTempEntry" - /> - </li> - <li> - <item-button - :label="__('New directory')" - class="d-flex" - icon="folder-new" - icon-classes="mr-2" - @click="createNewItem('tree')" + @click="deleteEntry(path)" /> </li> </ul> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 50ab242ba2a..6f1a941fbc4 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -44,7 +44,7 @@ export default { }, }, mounted() { - if (this.lastOpenedFile) { + if (this.lastOpenedFile && this.lastOpenedFile.type !== 'tree') { this.openPendingTab({ file: this.lastOpenedFile, keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged, diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 08ee12fd98f..f9badb01535 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -87,7 +87,9 @@ export default { this.editor.updateDimensions(); }, viewer() { - this.createEditorInstance(); + if (!this.file.pending) { + this.createEditorInstance(); + } }, panelResizing() { if (!this.panelResizing) { @@ -109,6 +111,7 @@ export default { }, methods: { ...mapActions([ + 'getFileData', 'getRawFileData', 'changeFileContent', 'setFileLanguage', @@ -123,10 +126,16 @@ export default { this.editor.clearEditor(); - this.getRawFileData({ + this.getFileData({ path: this.file.path, - baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', + makeFileActive: false, }) + .then(() => + this.getRawFileData({ + path: this.file.path, + baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', + }), + ) .then(() => { this.createEditorInstance(); }) @@ -246,6 +255,8 @@ export default { ref="editor" :class="{ 'is-readonly': isCommitModeActive, + 'is-deleted': file.deleted, + 'is-added': file.tempFile }" class="multi-file-editor-holder" > diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 3b4dd5ae9aa..eb4a927fe0d 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -34,11 +34,6 @@ export default { type: Number, required: true, }, - disableActionDropdown: { - type: Boolean, - required: false, - default: false, - }, }, data() { return { @@ -212,8 +207,7 @@ export default { /> </span> <new-dropdown - v-if="isTree && !disableActionDropdown" - :project-id="file.projectId" + :type="file.type" :branch="file.branchId" :path="file.path" :mouse-over="mouseOver" diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 03772ae4a4c..db47b75ec5c 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -37,7 +37,7 @@ export default { return this.fileHasChanged ? !this.tabMouseOver : false; }, fileHasChanged() { - return this.tab.changed || this.tab.tempFile || this.tab.staged; + return this.tab.changed || this.tab.tempFile || this.tab.staged || this.tab.deleted; }, }, @@ -71,7 +71,8 @@ export default { <template> <li :class="{ - active: tab.active + active: tab.active, + disabled: tab.pending }" @click="clickFile(tab)" @mouseover="mouseOverTab" @@ -105,7 +106,6 @@ export default { <changed-file-icon v-else :file="tab" - :force-modified-icon="true" /> </button> </li> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 45d36f6f42c..0b514f31467 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -38,3 +38,18 @@ export const stageKeys = { unstaged: 'unstaged', staged: 'staged', }; + +export const commitItemIconMap = { + addition: { + icon: 'file-addition', + class: 'ide-file-addition', + }, + modified: { + icon: 'file-modified', + class: 'ide-file-modified', + }, + deleted: { + icon: 'file-deletion', + class: 'ide-file-deletion', + }, +}; diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index 78e6f632728..60bddb34977 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -7,7 +7,7 @@ export default class Model { this.disposable = new Disposable(); this.file = file; this.head = head; - this.content = file.content !== '' ? file.content : file.raw; + this.content = file.content !== '' || file.deleted ? file.content : file.raw; this.disposable.add( (this.originalModel = monacoEditor.createModel( diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 49a481f25d5..cb93fba1665 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -18,7 +18,7 @@ export default { return axios .get(file.rawPath, { - params: { format: 'json' }, + transformResponse: [f => f], }) .then(({ data }) => data); }, @@ -33,7 +33,7 @@ export default { return axios .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), { - params: { format: 'json' }, + transformResponse: [f => f], }) .then(({ data }) => data); }, diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index b5bd6f5a6bb..2765acada48 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -185,6 +185,14 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { $('#ide-new-entry').modal('show'); }; +export const deleteEntry = ({ commit, dispatch, state }, path) => { + dispatch('burstUnusedSeal'); + dispatch('closeFile', state.entries[path]); + commit(types.DELETE_ENTRY, path); +}; + +export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 6c0887e11ee..b343750f789 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -61,7 +61,11 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { const file = state.entries[path]; + + if (file.raw || file.tempFile) return Promise.resolve(); + commit(types.TOGGLE_LOADING, { entry: file }); + return service .getFileData( `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, @@ -71,7 +75,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE'])); commit(types.SET_FILE_DATA, { data, file }); - commit(types.TOGGLE_FILE_OPEN, path); + if (makeFileActive) commit(types.TOGGLE_FILE_OPEN, path); if (makeFileActive) dispatch('setFileActive', path); commit(types.TOGGLE_LOADING, { entry: file }); }) @@ -97,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = service .getRawFileData(file) .then(raw => { - commit(types.SET_FILE_RAW_DATA, { file, raw }); + if (!file.tempFile) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (file.mrChange && file.mrChange.new_file === false) { service .getBaseRawFileData(file, baseSha) diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index ffaaaabff17..acb6ef5e6d4 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -21,14 +21,12 @@ export const showTreeEntry = ({ commit, dispatch, state }, path) => { export const handleTreeEntryAction = ({ commit, dispatch }, row) => { if (row.type === 'tree') { dispatch('toggleTreeOpen', row.path); - } else if (row.type === 'blob' && (row.opened || row.changed)) { - if (row.changed && !row.opened) { + } else if (row.type === 'blob') { + if (!row.opened) { commit(types.TOGGLE_FILE_OPEN, row.path); } dispatch('setFileActive', row.path); - } else { - dispatch('getFileData', { path: row.path }); } dispatch('showTreeEntry', row.path); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 7828c31f20e..462ca45db9b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -174,11 +174,13 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo dispatch('updateActivityBarView', activityBarViews.edit, { root: true }); dispatch('updateViewer', 'editor', { root: true }); - router.push( - `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ - rootGetters.activeFile.path - }`, - ); + if (rootGetters.activeFile) { + router.push( + `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ + rootGetters.activeFile.path + }`, + ); + } } }) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 3db4b2f903e..03777e6c10b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -1,7 +1,15 @@ -import { sprintf, n__ } from '../../../../locale'; +import { sprintf, n__, __ } from '../../../../locale'; import * as consts from './constants'; const BRANCH_SUFFIX_COUNT = 5; +const createTranslatedTextForFiles = (files, text) => { + if (!files.length) return null; + + return sprintf(n__('%{text} %{files}', '%{text} %{files} files', files.length), { + files: files.reduce((acc, val) => acc.concat(val.path), []).join(', '), + text, + }); +}; export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; @@ -29,14 +37,16 @@ export const branchName = (state, getters, rootState) => { export const preBuiltCommitMessage = (state, _, rootState) => { if (state.commitMessage) return state.commitMessage; - const files = (rootState.stagedFiles.length - ? rootState.stagedFiles - : rootState.changedFiles - ).reduce((acc, val) => acc.concat(val.path), []); + const files = rootState.stagedFiles.length ? rootState.stagedFiles : rootState.changedFiles; + const modifiedFiles = files.filter(f => !f.deleted); + const deletedFiles = files.filter(f => f.deleted); - return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), { - files: files.join(', '), - }); + return [ + createTranslatedTextForFiles(modifiedFiles, __('Update')), + createTranslatedTextForFiles(deletedFiles, __('Deleted')), + ] + .filter(t => t) + .join('\n'); }; // prevent babel-plugin-rewire from generating an invalid default during karma tests diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 8d6f9ccaf34..dae60f4d65a 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -76,3 +76,4 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; +export const DELETE_ENTRY = 'DELETE_ENTRY'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index f8091f5b5e0..799c2f51e8d 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; import projectMutations from './mutations/project'; import mergeRequestMutation from './mutations/merge_request'; @@ -171,6 +172,16 @@ export default { newEntryModal: { type, path }, }); }, + [types.DELETE_ENTRY](state, path) { + const entry = state.entries[path]; + const parent = entry.parentPath + ? state.entries[entry.parentPath] + : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + + entry.deleted = true; + state.changedFiles = state.changedFiles.concat(entry); + parent.tree = parent.tree.filter(f => f.path !== entry.path); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 46547820425..9a87d50d6d5 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; +import { sortTree } from '../utils'; import { diffModes } from '../../constants'; export default { @@ -51,9 +52,17 @@ export default { }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { + const openPendingFile = state.openFiles.find( + f => f.path === file.path && f.pending && !f.tempFile, + ); + Object.assign(state.entries[file.path], { raw, }); + + if (openPendingFile) { + openPendingFile.raw = raw; + } }, [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) { Object.assign(state.entries[file.path], { @@ -109,11 +118,22 @@ export default { }, [types.DISCARD_FILE_CHANGES](state, path) { const stagedFile = state.stagedFiles.find(f => f.path === path); + const entry = state.entries[path]; + const { deleted } = entry; Object.assign(state.entries[path], { content: stagedFile ? stagedFile.content : state.entries[path].raw, changed: false, + deleted: false, }); + + if (deleted) { + const parent = entry.parentPath + ? state.entries[entry.parentPath] + : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + + parent.tree = sortTree(parent.tree.concat(entry)); + } }, [types.ADD_FILE_TO_CHANGED](state, path) { Object.assign(state, { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 9e6b86dd844..bf7ab93ff5e 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -46,6 +46,7 @@ export const dataStructure = () => ({ parentPath: null, lastOpenedAt: 0, mrChange: null, + deleted: false, }); export const decorateData = entity => { @@ -105,15 +106,37 @@ export const setPageTitle = title => { document.title = title; }; +export const commitActionForFile = file => { + if (file.deleted) { + return 'delete'; + } else if (file.tempFile) { + return 'create'; + } + + return 'update'; +}; + +export const getCommitFiles = (stagedFiles, deleteTree = false) => + stagedFiles.reduce((acc, file) => { + if ((file.deleted || deleteTree) && file.type === 'tree') { + return acc.concat(getCommitFiles(file.tree, true)); + } + + return acc.concat({ + ...file, + deleted: deleteTree || file.deleted, + }); + }, []); + export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({ branch, commit_message: state.commitMessage || getters.preBuiltCommitMessage, - actions: rootState.stagedFiles.map(f => ({ - action: f.tempFile ? 'create' : 'update', + actions: getCommitFiles(rootState.stagedFiles).map(f => ({ + action: commitActionForFile(f), file_path: f.path, content: f.content, encoding: f.base64 ? 'base64' : 'text', - last_commit_id: newBranch ? undefined : f.lastCommitSha, + last_commit_id: newBranch || f.deleted ? undefined : f.lastCommitSha, })), start_branch: newBranch ? rootState.currentBranchId : undefined, }); diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js new file mode 100644 index 00000000000..92b15cf232d --- /dev/null +++ b/app/assets/javascripts/ide/utils.js @@ -0,0 +1,12 @@ +import { commitItemIconMap } from './constants'; + +// eslint-disable-next-line import/prefer-default-export +export const getCommitIconMap = file => { + if (file.deleted) { + return commitItemIconMap.deleted; + } else if (file.tempFile) { + return commitItemIconMap.addition; + } + + return commitItemIconMap.modified; +}; diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index c32dc83da8e..14518f86dc7 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import _ from 'underscore'; import JobNameComponent from './job_name_component.vue'; import JobComponent from './job_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -46,7 +47,7 @@ export default { computed: { tooltipText() { - return `${this.job.name} - ${this.job.status.label}`; + return _.escape(`${this.job.name} - ${this.job.status.label}`); }, }, diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 4ec67f6c01b..1952dd453f4 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import StageColumnComponent from './stage_column_component.vue'; @@ -26,7 +27,8 @@ export default { methods: { capitalizeStageName(name) { - return name.charAt(0).toUpperCase() + name.slice(1); + const escapedName = _.escape(name); + return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); }, isFirstColumn(index) { diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 8af984ef91a..84a3d58b770 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import ActionComponent from './action_component.vue'; import JobNameComponent from './job_name_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -61,7 +62,7 @@ export default { const textBuilder = []; if (this.job.name) { - textBuilder.push(this.job.name); + textBuilder.push(_.escape(this.job.name)); } if (this.job.name && this.status.tooltip) { @@ -69,7 +70,7 @@ export default { } if (this.status.tooltip) { - textBuilder.push(`${this.job.status.tooltip}`); + textBuilder.push(this.job.status.tooltip); } return textBuilder.join(' '); diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 2c728582b7c..e7b2de52f76 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import JobComponent from './job_component.vue'; import DropdownJobComponent from './dropdown_job_component.vue'; @@ -37,7 +38,7 @@ export default { }, jobId(job) { - return `ci-badge-${job.name}`; + return `ci-badge-${_.escape(job.name)}`; }, buildConnnectorClass(index) { diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index d335c3f55af..dc599e1b9fc 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -42,11 +42,14 @@ export default { return this.timeEstimate - this.timeSpent; }, timeRemainingPercent() { - return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; + return Math.floor((this.timeSpent / this.timeEstimate) * 100); }, timeRemainingStatusClass() { return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; }, + progressBarVariant() { + return this.timeRemainingPercent > 100 ? 'danger' : 'primary'; + }, }, }; </script> @@ -62,16 +65,10 @@ export default { data-placement="top" role="timeRemainingDisplay" > - <div - :aria-valuenow="timeRemainingPercent" - class="meter-container" - > - <div - :style="{ width: timeRemainingPercent }" - class="meter-fill" - > - </div> - </div> + <gl-progress-bar + :value="timeRemainingPercent" + :variant="progressBarVariant" + /> <div class="compare-display-container"> <div class="compare-display float-left"> <span class="compare-label"> diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue new file mode 100644 index 00000000000..3ced4eb691a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue @@ -0,0 +1,391 @@ +<script> +import * as d3 from 'd3'; +import tooltip from '../directives/tooltip'; +import Icon from './icon.vue'; +import SvgGradient from './svg_gradient.vue'; +import { + GRADIENT_COLORS, + GRADIENT_OPACITY, + INVERSE_GRADIENT_COLORS, + INVERSE_GRADIENT_OPACITY, +} from './bar_chart_constants'; + +/** + * Renders a bar chart that can be dragged(scrolled) when the number + * of elements to renders surpasses that of the available viewport space + * while keeping even padding and a width of 24px (customizable) + * + * It can render data with the following format: + * graphData: [{ + * name: 'element' // x domain data + * value: 1 // y domain data + * }] + * + * Used in: + * - Contribution analytics - all of the rows describing pushes, merge requests and issues + */ + +export default { + directives: { + tooltip, + }, + components: { + Icon, + SvgGradient, + }, + props: { + graphData: { + type: Array, + required: true, + }, + barWidth: { + type: Number, + required: false, + default: 24, + }, + yAxisLabel: { + type: String, + required: true, + }, + }, + data() { + return { + minX: -40, + minY: 0, + vbWidth: 0, + vbHeight: 0, + vpWidth: 0, + vpHeight: 350, + preserveAspectRatioType: 'xMidYMid meet', + containerMargin: { + leftRight: 30, + }, + viewBoxMargin: { + topBottom: 150, + }, + panX: 0, + xScale: {}, + yScale: {}, + zoom: {}, + bars: {}, + xGraphRange: 0, + isLoading: true, + paddingThreshold: 50, + showScrollIndicator: false, + showLeftScrollIndicator: false, + isGrabbed: false, + isPanAvailable: false, + gradientColors: GRADIENT_COLORS, + gradientOpacity: GRADIENT_OPACITY, + inverseGradientColors: INVERSE_GRADIENT_COLORS, + inverseGradientOpacity: INVERSE_GRADIENT_OPACITY, + maxTextWidth: 72, + rectYAxisLabelDims: {}, + xAxisTextElements: {}, + yAxisRectTransformPadding: 20, + yAxisTextTransformPadding: 10, + yAxisTextRotation: 90, + }; + }, + computed: { + svgViewBox() { + return `${this.minX} ${this.minY} ${this.vbWidth} ${this.vbHeight}`; + }, + xAxisLocation() { + return `translate(${this.panX}, ${this.vbHeight})`; + }, + barTranslationTransform() { + return `translate(${this.panX}, 0)`; + }, + scrollIndicatorTransform() { + return `translate(${this.vbWidth - 80}, 0)`; + }, + activateGrabCursor() { + return { + 'svg-graph-container-with-grab': this.isPanAvailable, + 'svg-graph-container-grabbed': this.isPanAvailable && this.isGrabbed, + }; + }, + yAxisLabelRectTransform() { + const rectWidth = + this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0; + const yCoord = this.vbHeight / 2 - rectWidth; + + return `translate(${this.minX - this.yAxisRectTransformPadding}, ${yCoord})`; + }, + yAxisLabelTextTransform() { + const rectWidth = + this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0; + const yCoord = this.vbHeight / 2 + rectWidth - 5; + + return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${this.yAxisTextRotation})`; + }, + }, + mounted() { + if (!this.allValuesEmpty) { + this.draw(); + } + }, + methods: { + draw() { + // update viewport + this.vpWidth = this.$refs.svgContainer.clientWidth - this.containerMargin.leftRight; + // update viewbox + this.vbWidth = this.vpWidth; + this.vbHeight = this.vpHeight - this.viewBoxMargin.topBottom; + let padding = 0; + if (this.graphData.length * this.barWidth > this.vbWidth) { + this.xGraphRange = this.graphData.length * this.barWidth; + padding = this.calculatePadding(this.barWidth); + this.showScrollIndicator = true; + this.isPanAvailable = true; + } else { + this.xGraphRange = this.vbWidth - Math.abs(this.minX); + } + + this.xScale = d3 + .scaleBand() + .range([0, this.xGraphRange]) + .round(true) + .paddingInner(padding); + this.yScale = d3.scaleLinear().rangeRound([this.vbHeight, 0]); + + this.xScale.domain(this.graphData.map(d => d.name)); + this.yScale.domain([0, d3.max(this.graphData.map(d => d.value))]); + + // Zoom/Panning Function + this.zoom = d3 + .zoom() + .translateExtent([[0, 0], [this.xGraphRange, this.vbHeight]]) + .on('zoom', this.panGraph) + .on('end', this.removeGrabStyling); + + const xAxis = d3.axisBottom().scale(this.xScale); + + const yAxis = d3 + .axisLeft() + .scale(this.yScale) + .ticks(4); + + const renderedXAxis = d3 + .select(this.$refs.baseSvg) + .select('.x-axis') + .call(xAxis); + + this.xAxisTextElements = this.$refs.xAxis.querySelectorAll('text'); + + renderedXAxis.select('.domain').remove(); + + renderedXAxis + .selectAll('text') + .style('text-anchor', 'end') + .attr('dx', '-.3em') + .attr('dy', '-.95em') + .attr('class', 'tick-text') + .attr('transform', 'rotate(-90)'); + + renderedXAxis.selectAll('line').remove(); + + const { maxTextWidth } = this; + renderedXAxis.selectAll('text').each(function formatText() { + const axisText = d3.select(this); + let textLength = axisText.node().getComputedTextLength(); + let textContent = axisText.text(); + while (textLength > maxTextWidth && textContent.length > 0) { + textContent = textContent.slice(0, -1); + axisText.text(`${textContent}...`); + textLength = axisText.node().getComputedTextLength(); + } + }); + + const width = this.vbWidth; + + const renderedYAxis = d3 + .select(this.$refs.baseSvg) + .select('.y-axis') + .call(yAxis); + + renderedYAxis.selectAll('.tick').each(function createTickLines(d, i) { + if (i > 0) { + d3 + .select(this) + .select('line') + .attr('x2', width) + .attr('class', 'axis-tick'); + } + }); + + // Add the panning capabilities + if (this.isPanAvailable) { + d3 + .select(this.$refs.baseSvg) + .call(this.zoom) + .on('wheel.zoom', null); // This disables the pan of the graph with the scroll of the mouse wheel + } + + this.isLoading = false; + // Update the yAxisLabel coordinates + const labelDims = this.$refs.yAxisLabel.getBBox(); + this.rectYAxisLabelDims = { + height: labelDims.width + 10, + }; + }, + panGraph() { + const allowedRightScroll = this.xGraphRange - this.vbWidth - this.paddingThreshold; + const graphMaxPan = Math.abs(d3.event.transform.x) < allowedRightScroll; + this.isGrabbed = true; + this.panX = d3.event.transform.x; + + if (d3.event.transform.x === 0) { + this.showLeftScrollIndicator = false; + } else { + this.showLeftScrollIndicator = true; + this.showScrollIndicator = true; + } + + if (!graphMaxPan) { + this.panX = -1 * (this.xGraphRange - this.vbWidth + this.paddingThreshold); + this.showScrollIndicator = false; + } + }, + setTooltipTitle(data) { + return data !== null ? `${data.name}: ${data.value}` : ''; + }, + calculatePadding(desiredBarWidth) { + const widthWithMargin = this.vbWidth - Math.abs(this.minX); + const dividend = widthWithMargin - this.graphData.length * desiredBarWidth; + const divisor = widthWithMargin - desiredBarWidth; + + return dividend / divisor; + }, + removeGrabStyling() { + this.isGrabbed = false; + }, + barHoveredIn(index) { + this.xAxisTextElements[index].classList.add('x-axis-text'); + }, + barHoveredOut(index) { + this.xAxisTextElements[index].classList.remove('x-axis-text'); + }, + }, +}; +</script> +<template> + <div + ref="svgContainer" + :class="activateGrabCursor" + class="svg-graph-container" + > + <svg + ref="baseSvg" + :width="vpWidth" + :height="vpHeight" + :viewBox="svgViewBox" + :preserveAspectRatio="preserveAspectRatioType"> + <g + ref="xAxis" + :transform="xAxisLocation" + class="x-axis" + /> + <g v-if="!isLoading"> + <template + v-for="(data, index) in graphData"> + <rect + v-tooltip + :key="index" + :width="xScale.bandwidth()" + :x="xScale(data.name)" + :y="yScale(data.value)" + :height="vbHeight - yScale(data.value)" + :transform="barTranslationTransform" + :title="setTooltipTitle(data)" + class="bar-rect" + data-placement="top" + @mouseover="barHoveredIn(index)" + @mouseout="barHoveredOut(index)" + /> + </template> + </g> + <rect + :height="vbHeight + 100" + transform="translate(-100, -5)" + width="100" + fill="#fff" + /> + <g class="y-axis-label"> + <line + :x1="0" + :x2="0" + :y1="0" + :y2="vbHeight" + transform="translate(-35, 0)" + stroke="black" + /> + <!--Get text length and change the height of this rect accordingly--> + <rect + :height="rectYAxisLabelDims.height" + :transform="yAxisLabelRectTransform" + :width="30" + fill="#fff" + /> + <text + ref="yAxisLabel" + :transform="yAxisLabelTextTransform" + > + {{ yAxisLabel }} + </text> + </g> + <g + class="y-axis" + /> + <g v-if="showScrollIndicator"> + <rect + :height="vbHeight + 100" + :transform="`translate(${vpWidth - 60}, -5)`" + width="40" + fill="#fff" + /> + <icon + :x="vpWidth - 50" + :y="vbHeight / 2" + :width="14" + :height="14" + name="chevron-right" + class="animate-flicker" + /> + </g> + <!--The line that shows up when the data elements surpass the available width --> + <g + v-if="showScrollIndicator" + :transform="scrollIndicatorTransform"> + <rect + :height="vbHeight" + x="0" + y="0" + width="20" + fill="url(#shadow-gradient)" + /> + </g> + <!--Left scroll indicator--> + <g + v-if="showLeftScrollIndicator" + transform="translate(0, 0)"> + <rect + :height="vbHeight" + x="0" + y="0" + width="20" + fill="url(#left-shadow-gradient)" + /> + </g> + <svg-gradient + :colors="gradientColors" + :opacity="gradientOpacity" + identifier-name="shadow-gradient"/> + <svg-gradient + :colors="inverseGradientColors" + :opacity="inverseGradientOpacity" + identifier-name="left-shadow-gradient"/> + </svg> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/bar_chart_constants.js b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js new file mode 100644 index 00000000000..6957b112da6 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js @@ -0,0 +1,4 @@ +export const GRADIENT_COLORS = ['#000', '#a7a7a7']; +export const GRADIENT_OPACITY = ['0', '0.4']; +export const INVERSE_GRADIENT_COLORS = ['#a7a7a7', '#000']; +export const INVERSE_GRADIENT_OPACITY = ['0.4', '0']; diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index 8c2dcc2d902..7947ae1e4da 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -32,7 +32,7 @@ }, computed: { className() { - return `drag${this.side}`; + return `drag-${this.side}`; }, cursorStyle() { if (this.enabled) { @@ -44,8 +44,15 @@ methods: { resetSize(e) { e.preventDefault(); + this.$emit('resize-start', this.size); + this.size = this.startSize; this.$emit('update:size', this.size); + + // End resizing on next tick so that listeners can react to DOM changes + this.$nextTick(() => { + this.$emit('resize-end', this.size); + }); }, startDrag(e) { if (this.enabled) { @@ -84,7 +91,7 @@ <div :class="className" :style="cursorStyle" - class="dragHandle" + class="drag-handle" @mousedown="startDrag" @dblclick="resetSize" ></div> diff --git a/app/assets/javascripts/vue_shared/components/reports/constants.js b/app/assets/javascripts/vue_shared/components/reports/constants.js new file mode 100644 index 00000000000..dbde648bfdb --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/constants.js @@ -0,0 +1,3 @@ +export const STATUS_FAILED = 'failed'; +export const STATUS_SUCCESS = 'success'; +export const STATUS_NEUTRAL = 'neutral'; diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_body.js b/app/assets/javascripts/vue_shared/components/reports/issue_body.js new file mode 100644 index 00000000000..f2141e519da --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/issue_body.js @@ -0,0 +1,3 @@ +export const components = {}; + +export const componentNames = {}; diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue b/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue new file mode 100644 index 00000000000..f8189117ac3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue @@ -0,0 +1,58 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +import { + STATUS_FAILED, + STATUS_NEUTRAL, + STATUS_SUCCESS, +} from '~/vue_shared/components/reports/constants'; + +export default { + name: 'IssueStatusIcon', + components: { + Icon, + }, + props: { + // failed || success + status: { + type: String, + required: true, + }, + }, + computed: { + iconName() { + if (this.isStatusFailed) { + return 'status_failed_borderless'; + } else if (this.isStatusSuccess) { + return 'status_success_borderless'; + } + + return 'status_created_borderless'; + }, + isStatusFailed() { + return this.status === STATUS_FAILED; + }, + isStatusSuccess() { + return this.status === STATUS_SUCCESS; + }, + isStatusNeutral() { + return this.status === STATUS_NEUTRAL; + }, + }, +}; +</script> +<template> + <div + :class="{ + failed: isStatusFailed, + success: isStatusSuccess, + neutral: isStatusNeutral, + }" + class="report-block-list-icon" + > + <icon + :name="iconName" + :size="32" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue index e1e03e39ee0..c01f77c2509 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue +++ b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue @@ -1,5 +1,10 @@ <script> -import IssuesBlock from './report_issues.vue'; +import IssuesBlock from '~/vue_shared/components/reports/report_issues.vue'; +import { + STATUS_SUCCESS, + STATUS_FAILED, + STATUS_NEUTRAL, +} from '~/vue_shared/components/reports/constants'; /** * Renders block of issues @@ -9,6 +14,9 @@ export default { components: { IssuesBlock, }, + success: STATUS_SUCCESS, + failed: STATUS_FAILED, + neutral: STATUS_NEUTRAL, props: { unresolvedIssues: { type: Array, @@ -25,29 +33,10 @@ export default { required: false, default: () => [], }, - allIssues: { - type: Array, - required: false, - default: () => [], - }, - type: { + component: { type: String, - required: true, - }, - }, - data() { - return { - isFullReportVisible: false, - }; - }, - computed: { - unresolvedIssuesStatus() { - return this.type === 'license' ? 'neutral' : 'failed'; - }, - }, - methods: { - openFullReport() { - this.isFullReportVisible = true; + required: false, + default: '', }, }, }; @@ -57,43 +46,26 @@ export default { <issues-block v-if="unresolvedIssues.length" - :type="type" - :status="unresolvedIssuesStatus" + :component="component" :issues="unresolvedIssues" + :status="$options.failed" class="js-mr-code-new-issues" /> <issues-block - v-if="isFullReportVisible" - :type="type" - :issues="allIssues" - class="js-mr-code-all-issues" - status="failed" - /> - - <issues-block v-if="neutralIssues.length" - :type="type" + :component="component" :issues="neutralIssues" + :status="$options.neutral" class="js-mr-code-non-issues" - status="neutral" /> <issues-block v-if="resolvedIssues.length" - :type="type" + :component="component" :issues="resolvedIssues" + :status="$options.success" class="js-mr-code-resolved-issues" - status="success" /> - - <button - v-if="allIssues.length && !isFullReportVisible" - type="button" - class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link" - @click="openFullReport" - > - {{ s__("ciReport|Show complete code vulnerabilities report") }} - </button> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue index ecffb02a3a0..2d1f3d82234 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue +++ b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue @@ -1,19 +1,23 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import IssueStatusIcon from '~/vue_shared/components/reports/issue_status_icon.vue'; +import { components, componentNames } from '~/vue_shared/components/reports/issue_body'; export default { name: 'ReportIssues', components: { - Icon, + IssueStatusIcon, + ...components, }, props: { issues: { type: Array, required: true, }, - type: { + component: { type: String, - required: true, + required: false, + default: '', + validator: value => value === '' || Object.values(componentNames).includes(value), }, // failed || success status: { @@ -21,26 +25,6 @@ export default { required: true, }, }, - computed: { - iconName() { - if (this.isStatusFailed) { - return 'status_failed_borderless'; - } else if (this.isStatusSuccess) { - return 'status_success_borderless'; - } - - return 'status_created_borderless'; - }, - isStatusFailed() { - return this.status === 'failed'; - }, - isStatusSuccess() { - return this.status === 'success'; - }, - isStatusNeutral() { - return this.status === 'neutral'; - }, - }, }; </script> <template> @@ -52,20 +36,17 @@ export default { :key="index" class="report-block-list-issue" > - <div - :class="{ - failed: isStatusFailed, - success: isStatusSuccess, - neutral: isStatusNeutral, - }" - class="report-block-list-icon append-right-5" - > - <icon - :name="iconName" - :size="32" - /> - </div> + <issue-status-icon + :status="issue.status || status" + class="append-right-5" + /> + <component + v-if="component" + :is="component" + :issue="issue" + :status="issue.status || status" + /> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/reports/report_section.vue b/app/assets/javascripts/vue_shared/components/reports/report_section.vue index d383ed99a0c..0124d8b5bcc 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_section.vue +++ b/app/assets/javascripts/vue_shared/components/reports/report_section.vue @@ -21,7 +21,7 @@ export default { required: false, default: false, }, - type: { + component: { type: String, required: false, default: '', @@ -59,11 +59,6 @@ export default { required: false, default: () => [], }, - allIssues: { - type: Array, - required: false, - default: () => [], - }, infoText: { type: [String, Boolean], required: false, @@ -142,18 +137,10 @@ export default { </script> <template> <section class="media-section"> - <div - class="media" - > - <status-icon - :status="statusIconName" - /> - <div - class="media-body space-children d-flex" - > - <span - class="js-code-text code-text" - > + <div class="media"> + <status-icon :status="statusIconName" /> + <div class="media-body space-children d-flex flex-align-self-center"> + <span class="js-code-text code-text"> {{ headerText }} <popover @@ -163,10 +150,12 @@ export default { /> </span> + <slot name="actionButtons"></slot> + <button v-if="isCollapsible" type="button" - class="js-collapse-btn btn bt-default float-right btn-sm" + class="js-collapse-btn btn float-right btn-sm" @click="toggleCollapsed" > {{ collapseText }} @@ -183,8 +172,8 @@ export default { <issues-list :unresolved-issues="unresolvedIssues" :resolved-issues="resolvedIssues" - :all-issues="allIssues" - :type="type" + :neutral-issues="neutralIssues" + :component="component" /> </slot> </div> diff --git a/app/assets/javascripts/vue_shared/components/svg_gradient.vue b/app/assets/javascripts/vue_shared/components/svg_gradient.vue new file mode 100644 index 00000000000..b61a1befcd6 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/svg_gradient.vue @@ -0,0 +1,37 @@ +<script> +export default { + props: { + colors: { + type: Array, + required: true, + }, + opacity: { + type: Array, + required: true, + }, + identifierName: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <svg + height="0" + width="0"> + <defs> + <linearGradient + :id="identifierName"> + <stop + :stop-color="colors[0]" + :stop-opacity="opacity[0]" + offset="0%" /> + <stop + :stop-color="colors[1]" + :stop-opacity="opacity[1]" + offset="100%" /> + </linearGradient> + </defs> + </svg> +</template> diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 637587de597..2d6dba52801 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -370,11 +370,14 @@ img.emoji { margin-right: 10px; } -.alert, -.progress { +.alert { margin-bottom: $gl-padding; } +.progress { + height: 4px; +} + .project-item-select-holder { display: inline-block; position: relative; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2097bcebf69..e7e13d35d8e 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -567,9 +567,6 @@ border-bottom: 1px solid $white-normal; .mx-auto { - margin: 8px 0; - text-align: center; - .tanuki-logo, img { height: 36px; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 56307777a72..a2789021ab4 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -179,6 +179,10 @@ font-weight: inherit; } + a > code { + color: $gl-link-color; + } + dd { margin-left: $gl-padding; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index efc54196b75..08755b4b545 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -835,3 +835,5 @@ $font-family-monospace: $monospace-font; $input-line-height: 20px; $btn-line-height: 20px; $table-accent-bg: $gray-light; +$card-border-color: $border-color; +$card-cap-bg: $gray-light; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/page_bundles/ide.scss index 2d76f0ce004..442aef124d3 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1,3 +1,6 @@ +@import 'framework/variables'; +@import 'framework/mixins'; + .project-refs-form, .project-refs-target-form { display: inline-block; @@ -74,6 +77,7 @@ .ide-file-icon-holder { display: flex; align-items: center; + color: $theme-gray-700; } .ide-file-changed-icon { @@ -161,12 +165,23 @@ background-color: $white-light; border-bottom-color: $white-light; } + + &:not(.disabled) { + .multi-file-tab { + cursor: pointer; + } + } + + &.disabled { + .multi-file-tab-close { + cursor: default; + } + } } } .multi-file-tab { @include str-truncated(141px); - cursor: pointer; svg { vertical-align: middle; @@ -241,6 +256,38 @@ } } + .is-deleted { + .editor.modified { + .margin-view-overlays, + .lines-content, + .decorationsOverviewRuler { + // !important to override monaco inline styles + display: none !important; + } + } + + .diffOverviewRuler.modified { + // !important to override monaco inline styles + display: none !important; + } + } + + .is-added { + .editor.original { + .margin-view-overlays, + .lines-content, + .decorationsOverviewRuler { + // !important to override monaco inline styles + display: none !important; + } + } + + .diffOverviewRuler.original { + // !important to override monaco inline styles + display: none !important; + } + } + .monaco-diff-editor.vs { .editor.modified { box-shadow: none; @@ -557,16 +604,21 @@ } } -.multi-file-addition, -.multi-file-addition-solid { +.ide-file-addition, +.ide-file-addition-solid { color: $green-500; } -.multi-file-modified, -.multi-file-modified-solid { +.ide-file-modified, +.ide-file-modified-solid { color: $orange-500; } +.ide-file-deletion, +.ide-file-deletion-solid { + color: $red-500; +} + .multi-file-commit-list-collapsed { display: flex; flex-direction: column; @@ -781,18 +833,21 @@ } } -.dragHandle { +.drag-handle { position: absolute; top: 0; bottom: 0; - width: 1px; - background-color: $white-dark; + width: 4px; + + &:hover { + background-color: $white-normal; + } - &.dragright { + &.drag-right { right: 0; } - &.dragleft { + &.drag-left { left: 0; } } @@ -1014,6 +1069,10 @@ .ide-new-btn { margin-left: auto; } + + button { + color: $gl-text-color; + } } .ide-sidebar-branch-title { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f030189af06..e5c38a20bf0 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -270,6 +270,7 @@ .block { width: 100%; + word-break: break-word; &:last-child { border-bottom: 1px solid $border-gray-normal; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b616357bb8d..591e21243ed 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -16,6 +16,7 @@ svg { vertical-align: middle; + top: -1px; } } diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 84da9180f93..49d8a5d959b 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -31,3 +31,61 @@ color: $gl-text-red; } } + +.svg-graph-container { + width: 100%; + + .axis-tick { + opacity: 0.4; + } + + .tick-text { + fill: $gl-text-color-secondary; + } + + .x-axis-text { + fill: $theme-gray-900; + } + + .bar-rect { + fill: rgba($blue-500, 0.1); + stroke: $blue-500; + } + + .bar-rect:hover { + fill: rgba($blue-700, 0.3); + } + + .y-axis-label { + line { + stroke: $stat-graph-axis-fill; + } + + text { + font-weight: bold; + font-size: 12px; + fill: $theme-gray-800; + } + } +} + +.svg-graph-container-with-grab { + cursor: grab; + cursor: -webkit-grab; +} + +.svg-graph-container-grabbed { + cursor: grabbing; + cursor: -webkit-grabbing; +} + +@keyframes flickerAnimation { + 0% { opacity: 1; } + 50% { opacity: 0; } + 100% { opacity: 1; } +} + +.animate-flicker { + animation: flickerAnimation 1.5s infinite; + fill: $theme-gray-500; +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 797b106de23..d5ae2b673d9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -834,17 +834,7 @@ } .compare-meter { - &.within_estimate { - .meter-fill { - background: $gl-primary; - } - } - &.over_estimate { - .meter-fill { - background: $red-500; - } - .time-remaining, .compare-value.spent { color: $red-500; @@ -852,18 +842,6 @@ } } - .meter-container { - background: $border-gray-light; - border-radius: 3px; - - .meter-fill { - max-width: 100%; - height: 5px; - border-radius: 3px; - background: $gl-primary; - } - } - .compare-display-container { display: flex; justify-content: space-between; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index c1b1d2e028d..8a4a2caa6c9 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -237,7 +237,7 @@ } .login-page-broadcast { - margin-top: 50px; + margin-top: 40px; } .navless-container { diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss index 64110f9c3a0..bd777c66b56 100644 --- a/app/assets/stylesheets/snippets.scss +++ b/app/assets/stylesheets/snippets.scss @@ -22,8 +22,8 @@ height: 16px; background-size: cover; - &.gl-snippet-icon-doc_code { background-position: 0 0; } - &.gl-snippet-icon-doc_text { background-position: 0 -16px; } + &.gl-snippet-icon-doc-code { background-position: 0 0; } + &.gl-snippet-icon-doc-text { background-position: 0 -16px; } &.gl-snippet-icon-download { background-position: 0 -32px; } } diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 9e495061f4e..36faea8056e 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -4,13 +4,17 @@ class Projects::CommitsController < Projects::ApplicationController include ExtractsPath include RendersCommits - before_action :whitelist_query_limiting + before_action :whitelist_query_limiting, except: :commits_root before_action :require_non_empty_project - before_action :assign_ref_vars + before_action :assign_ref_vars, except: :commits_root before_action :authorize_download_code! - before_action :set_commits + before_action :set_commits, except: :commits_root before_action :set_request_format, only: :show + def commits_root + redirect_to project_commits_path(@project, @project.default_branch) + end + def show @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 21d3c918581..ce03b2d8d1d 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController begin return render_404 unless promote_service.execute(@label) - flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe + flash[:notice] = flash_notice_for(@label, @project.group) respond_to do |format| format.html do redirect_to(project_labels_path(@project), status: :see_other) @@ -135,6 +135,15 @@ class Projects::LabelsController < Projects::ApplicationController end end + def flash_notice_for(label, group) + notice = ''.html_safe + notice << label.title + notice << ' promoted to ' + notice << view_context.link_to('<u>group label</u>'.html_safe, group_labels_path(group)) + notice << '.' + notice + end + protected def label_params diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 5e86ec93f34..b9b3dcd5a85 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -76,8 +76,8 @@ class Projects::MilestonesController < Projects::ApplicationController def promote promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) + flash[:notice] = flash_notice_for(promoted_milestone, project.group) - flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\"><u>group milestone</u></a>.".html_safe respond_to do |format| format.html do redirect_to project_milestones_path(project) @@ -90,6 +90,15 @@ class Projects::MilestonesController < Projects::ApplicationController redirect_to milestone, alert: error.message end + def flash_notice_for(milestone, group) + notice = ''.html_safe + notice << milestone.title + notice << ' promoted to ' + notice << view_context.link_to('<u>group milestone</u>'.html_safe, group_milestone_path(group, milestone.iid)) + notice << '.' + notice + end + def destroy return access_denied! unless can?(current_user, :admin_milestone, @project) diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb index 53b77f5fed9..543bf1a1415 100644 --- a/app/finders/admin/projects_finder.rb +++ b/app/finders/admin/projects_finder.rb @@ -7,7 +7,7 @@ class Admin::ProjectsFinder end def execute - items = Project.without_deleted.with_statistics + items = Project.without_deleted.with_statistics.with_route items = by_namespace_id(items) items = by_visibilty_level(items) items = by_with_push(items) @@ -16,7 +16,7 @@ class Admin::ProjectsFinder items = by_archived(items) items = by_personal(items) items = by_name(items) - items = items.includes(namespace: [:owner]) + items = items.includes(namespace: [:owner, :route]) sort(items).page(params[:page]) end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index d9f9129d08a..8755a1a62e7 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -7,5 +7,5 @@ class GitlabSchema < GraphQL::Schema query(Types::QueryType) default_max_page_size 100 - # mutation(Types::MutationType) + mutation(Types::MutationType) end diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb new file mode 100644 index 00000000000..eb03dfe1624 --- /dev/null +++ b/app/graphql/mutations/base_mutation.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Mutations + class BaseMutation < GraphQL::Schema::RelayClassicMutation + field :errors, [GraphQL::STRING_TYPE], + null: false, + description: "Reasons why the mutation failed." + + def current_user + context[:current_user] + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/resolves_project.rb b/app/graphql/mutations/concerns/mutations/resolves_project.rb new file mode 100644 index 00000000000..0dd1f264a52 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/resolves_project.rb @@ -0,0 +1,13 @@ +module Mutations + module ResolvesProject + extend ActiveSupport::Concern + + def resolve_project(full_path:) + resolver.resolve(full_path: full_path) + end + + def resolver + Resolvers::ProjectResolver.new(object: nil, context: context) + end + end +end diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb new file mode 100644 index 00000000000..2149e72e2df --- /dev/null +++ b/app/graphql/mutations/merge_requests/base.rb @@ -0,0 +1,32 @@ +module Mutations + module MergeRequests + class Base < BaseMutation + include Gitlab::Graphql::Authorize::AuthorizeResource + include Mutations::ResolvesProject + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: "The project the merge request to mutate is in" + + argument :iid, GraphQL::ID_TYPE, + required: true, + description: "The iid of the merge request to mutate" + + field :merge_request, + Types::MergeRequestType, + null: true, + description: "The merge request after mutation" + + authorize :update_merge_request + + private + + def find_object(project_path:, iid:) + project = resolve_project(full_path: project_path) + resolver = Resolvers::MergeRequestResolver.new(object: project, context: context) + + resolver.resolve(iid: iid) + end + end + end +end diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb new file mode 100644 index 00000000000..a2aa0c84ee4 --- /dev/null +++ b/app/graphql/mutations/merge_requests/set_wip.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Mutations + module MergeRequests + class SetWip < Base + graphql_name 'MergeRequestSetWip' + + argument :wip, + GraphQL::BOOLEAN_TYPE, + required: true, + description: <<~DESC + Whether or not to set the merge request as a WIP. + DESC + + def resolve(project_path:, iid:, wip: nil) + merge_request = authorized_find!(project_path: project_path, iid: iid) + project = merge_request.project + + ::MergeRequests::UpdateService.new(project, current_user, wip_event: wip_event(merge_request, wip)) + .execute(merge_request) + + { + merge_request: merge_request, + errors: merge_request.errors.full_messages + } + end + + private + + def wip_event(merge_request, wip) + wip ? 'wip' : 'unwip' + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 06ed91c1658..2b4ef299296 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -1,7 +1,11 @@ +# frozen_string_literal: true + module Types class MutationType < BaseObject + include Gitlab::Graphql::MountMutation + graphql_name "Mutation" - # TODO: Add Mutations as fields + mount_mutation Mutations::MergeRequests::SetWip end end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 4ce89f89fa9..c005ecbb56b 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -4,4 +4,23 @@ module EnvironmentsHelper endpoint: project_environments_path(@project, format: :json) } end + + def metrics_data(project, environment) + { + "settings-path" => edit_project_service_path(project, 'prometheus'), + "clusters-path" => project_clusters_path(project), + "current-environment-name": environment.name, + "documentation-path" => help_page_path('administration/monitoring/prometheus/index.md'), + "empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'), + "empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'), + "empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'), + "empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'), + "metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json), + "deployment-endpoint" => project_environment_deployments_path(project, environment, format: :json), + "environments-endpoint": project_environments_path(project, format: :json), + "project-path" => project_path(project), + "tags-path" => project_tags_path(project), + "has-metrics" => "#{environment.has_metrics?}" + } + end end diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb index 551b9cca6b1..0a356ba55d2 100644 --- a/app/helpers/hooks_helper.rb +++ b/app/helpers/hooks_helper.rb @@ -10,7 +10,7 @@ module HooksHelper trigger_human_name = trigger.to_s.tr('_', ' ').camelize - link_to path, rel: 'nofollow' do + link_to path, rel: 'nofollow', method: :post do content_tag(:span, trigger_human_name) end end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 733832c1bbb..a05640773ad 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -116,7 +116,7 @@ module SnippetsHelper raw_project_snippet_url(@snippet.project, @snippet) end - link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw' + link_to external_snippet_icon('doc-code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw' end def embedded_snippet_download_button diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 1db1482d6b7..0e1e39501f5 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -124,7 +124,7 @@ class Notify < BaseMailer fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze headers['References'] ||= [] - headers['References'] << fallback_reply_message_id + headers['References'].unshift(fallback_reply_message_id) @reply_by_email = true end @@ -158,7 +158,7 @@ class Notify < BaseMailer def mail_answer_thread(model, headers = {}) headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>" headers['In-Reply-To'] = message_id(model) - headers['References'] = message_id(model) + headers['References'] = [message_id(model)] headers[:subject]&.prepend('Re: ') diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 48137c2ed68..ea6ec4d6b03 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -21,6 +21,14 @@ module Clusters end end + def ready_status + [:installed] + end + + def ready? + ready_status.include?(status_name) + end + def chart 'stable/prometheus' end diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb index 18cbbd871a1..9c36f633395 100644 --- a/app/models/concerns/prometheus_adapter.rb +++ b/app/models/concerns/prometheus_adapter.rb @@ -24,11 +24,10 @@ module PrometheusAdapter def query(query_name, *args) return unless can_query? - query_class = Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query") + query_class = query_klass_for(query_name) + query_args = build_query_args(*args) - args.map!(&:id) - - with_reactive_cache(query_class.name, *args, &query_class.method(:transform_reactive_result)) + with_reactive_cache(query_class.name, *query_args, &query_class.method(:transform_reactive_result)) end # Cache metrics for specific environment @@ -44,5 +43,13 @@ module PrometheusAdapter rescue Gitlab::PrometheusClient::Error => err { success: false, result: err.message } end + + def query_klass_for(query_name) + Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query") + end + + def build_query_args(*args) + args.map(&:id) + end end end diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index be0a5b49012..9155d82d567 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -59,6 +59,9 @@ module ReactiveCaching raise NotImplementedError end + def reactive_cache_updated(*args) + end + def with_reactive_cache(*args, &blk) bootstrap = !within_reactive_cache_lifetime?(*args) Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime) @@ -81,8 +84,11 @@ module ReactiveCaching locking_reactive_cache(*args) do if within_reactive_cache_lifetime?(*args) enqueuing_update(*args) do - value = calculate_reactive_cache(*args) - Rails.cache.write(full_reactive_cache_key(*args), value) + key = full_reactive_cache_key(*args) + new_value = calculate_reactive_cache(*args) + old_value = Rails.cache.read(key) + Rails.cache.write(key, new_value) + reactive_cache_updated(*args) if new_value != old_value end end end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 0176a12a131..cb91f8fbac8 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -90,34 +90,17 @@ module Routable end def full_name - if route && route.name.present? - @full_name ||= route.name # rubocop:disable Gitlab/ModuleWithInstanceVariables - else - update_route if persisted? - - build_full_name - end + route&.name || build_full_name end - # Every time `project.namespace.becomes(Namespace)` is called for polymorphic_path, - # a new instance is instantiated, and we end up duplicating the same query to retrieve - # the route. Caching this per request ensures that even if we have multiple instances, - # we will not have to duplicate work, avoiding N+1 queries in some cases. def full_path - return uncached_full_path unless RequestStore.active? && persisted? - - RequestStore[full_path_key] ||= uncached_full_path + route&.path || build_full_path end def full_path_components full_path.split('/') end - def expires_full_path_cache - RequestStore.delete(full_path_key) if RequestStore.active? - @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables - end - def build_full_path if parent && path parent.full_path + '/' + path @@ -138,16 +121,6 @@ module Routable self.errors[:path].concat(route_path_errors) if route_path_errors end - def uncached_full_path - if route && route.path.present? - @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables - else - update_route if persisted? - - build_full_path - end - end - def full_name_changed? name_changed? || parent_changed? end @@ -156,10 +129,6 @@ module Routable path_changed? || parent_changed? end - def full_path_key - @full_path_key ||= "routable/full_path/#{self.class.name}/#{self.id}" - end - def build_full_name if parent && name parent.human_name + ' / ' + name @@ -168,18 +137,9 @@ module Routable end end - def update_route - return if Gitlab::Database.read_only? - - prepare_route - route.save - end - def prepare_route route || build_route(source: self) route.path = build_full_path route.name = build_full_name - @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables - @full_name = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables end end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index f66bdd529f1..f5225cd81ed 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -11,8 +11,6 @@ module Storage Namespace.find(parent_id_was) # raise NotFound early if needed end - expires_full_path_cache - move_repositories if parent_changed? @@ -34,13 +32,12 @@ module Storage begin send_update_instructions write_projects_repository_config - - true - rescue - # Returning false does not rollback after_* transaction but gives - # us information about failing some of tasks - false + rescue => e + # Raise if development/test environment, else just notify Sentry + Gitlab::Sentry.track_exception(e, extra: { full_path_was: full_path_was, full_path: full_path, action: 'move_dir' }) end + + true # false would cancel later callbacks but not rollback end # Hooks diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 7ab647abe93..fdbe95059e5 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -1,6 +1,7 @@ class DeployToken < ActiveRecord::Base include Expirable include TokenAuthenticatable + include PolicyActor add_authentication_token_field :token AVAILABLE_SCOPES = %i(read_repository read_registry).freeze @@ -58,10 +59,6 @@ class DeployToken < ActiveRecord::Base write_attribute(:expires_at, value.presence || Forever.date) end - def admin? - false - end - private def ensure_at_least_one_scope diff --git a/app/models/email.rb b/app/models/email.rb index d6516761f0a..15bdedeac33 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -25,6 +25,10 @@ class Email < ActiveRecord::Base self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) end + def accept_pending_invitations! + user.accept_pending_invitations! + end + # once email is confirmed, update the gpg signatures def update_invalid_gpg_signatures user.update_invalid_gpg_signatures if confirmed? diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 7034c633268..c1dc2f55346 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -304,7 +304,6 @@ class Namespace < ActiveRecord::Base def write_projects_repository_config all_projects.find_each do |project| - project.expires_full_path_cache # we need to clear cache to validate renames correctly project.write_repository_config end end diff --git a/app/models/project.rb b/app/models/project.rb index 325dbd0197f..a452ec5fcdf 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -31,6 +31,7 @@ class Project < ActiveRecord::Base BoardLimitExceeded = Class.new(StandardError) + STATISTICS_ATTRIBUTE = 'repositories_count'.freeze NUMBER_OF_PERMITTED_BOARDS = 1 UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze # Hashed Storage versions handle rolling out new storage to project and dependents models: @@ -79,6 +80,10 @@ class Project < ActiveRecord::Base after_create :create_project_feature, unless: :project_feature + after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) } + before_destroy ->(project) { project.project_feature.untrack_statistics_for_deletion! } + after_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) } + after_create :create_ci_cd_settings, unless: :ci_cd_settings, if: proc { ProjectCiCdSetting.available? } @@ -1235,8 +1240,6 @@ class Project < ActiveRecord::Base return true if skip_disk_validation return false unless repository_storage - expires_full_path_cache # we need to clear cache to validate renames correctly - # Check if repository with same path already exists on disk we can # skip this for the hashed storage because the path does not change if legacy_storage? && repository_with_same_path_already_exists? @@ -1615,7 +1618,6 @@ class Project < ActiveRecord::Base # When we import a project overwriting the original project, there # is a move operation. In that case we don't want to send the instructions. send_move_instructions(full_path_was) unless import_started? - expires_full_path_cache self.old_path_with_namespace = full_path_was SystemHooksService.new.execute_hooks_for(self, :rename) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index bfb8d703ec9..9c768b13f78 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -19,6 +19,7 @@ class ProjectFeature < ActiveRecord::Base ENABLED = 20 FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze + STATISTICS_ATTRIBUTE = 'wikis_count'.freeze class << self def access_level_attribute(feature) @@ -52,6 +53,9 @@ class ProjectFeature < ActiveRecord::Base default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false + after_create ->(model) { SiteStatistic.track(STATISTICS_ATTRIBUTE) if model.wiki_enabled? } + after_update :update_site_statistics + def feature_available?(feature, user) get_permission(user, access_level(feature)) end @@ -76,8 +80,30 @@ class ProjectFeature < ActiveRecord::Base issues_access_level > DISABLED end + # This is a workaround for the removal hooks not been triggered when removing a Project. + # + # ProjectFeature is removed using database cascade index rule. + # This method is called by Project model when deletion starts. + def untrack_statistics_for_deletion! + return unless wiki_enabled? + + SiteStatistic.untrack(STATISTICS_ATTRIBUTE) + end + private + def update_site_statistics + return unless wiki_access_level_changed? + + if self.wiki_access_level_was == DISABLED + # possible new states are PRIVATE / ENABLED, both should be tracked + SiteStatistic.track(STATISTICS_ATTRIBUTE) + elsif self.wiki_access_level == DISABLED + # old state was either PRIVATE / ENABLED, only untrack if new state is DISABLED + SiteStatistic.untrack(STATISTICS_ATTRIBUTE) + end + end + # Validates builds and merge requests access level # which cannot be higher than repository access level def repository_children_level diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 976b501e297..6172bb38881 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -48,13 +48,13 @@ class RemoteMirror < ActiveRecord::Base state :failed after_transition any => :started do |remote_mirror, _| - Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path) + Gitlab::Metrics.add_event(:remote_mirrors_running) remote_mirror.update(last_update_started_at: Time.now) end after_transition started: :finished do |remote_mirror, _| - Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path) + Gitlab::Metrics.add_event(:remote_mirrors_finished) timestamp = Time.now remote_mirror.update!( @@ -63,7 +63,7 @@ class RemoteMirror < ActiveRecord::Base end after_transition started: :failed do |remote_mirror, _| - Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path) + Gitlab::Metrics.add_event(:remote_mirrors_failed) remote_mirror.update(last_update_at: Time.now) end diff --git a/app/models/repository.rb b/app/models/repository.rb index e248f94cbd8..9873d9a6327 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1029,7 +1029,7 @@ class Repository end def repository_event(event, tags = {}) - Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags)) + Gitlab::Metrics.add_event(event, tags) end def initialize_raw_repository diff --git a/app/models/site_statistic.rb b/app/models/site_statistic.rb new file mode 100644 index 00000000000..9c9c3172fe6 --- /dev/null +++ b/app/models/site_statistic.rb @@ -0,0 +1,74 @@ +class SiteStatistic < ActiveRecord::Base + # prevents the creation of multiple rows + default_value_for :id, 1 + + COUNTER_ATTRIBUTES = %w(repositories_count wikis_count).freeze + REQUIRED_SCHEMA_VERSION = 20180629153018 + + # Tracks specific attribute + # + # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES + def self.track(raw_attribute) + with_statistics_available(raw_attribute) do |attribute| + SiteStatistic.update_all(["#{attribute} = #{attribute}+1"]) + end + end + + # Untracks specific attribute + # + # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES + def self.untrack(raw_attribute) + with_statistics_available(raw_attribute) do |attribute| + SiteStatistic.update_all(["#{attribute} = #{attribute}-1 WHERE #{attribute} > 0"]) + end + end + + # Wrapper for track/untrack operations with basic validations and enforced requirements + # + # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES + # @yield [String] attribute quoted to be used inside SQL / Arel query + def self.with_statistics_available(raw_attribute) + unless raw_attribute.in?(COUNTER_ATTRIBUTES) + raise ArgumentError, "Invalid attribute: '#{raw_attribute}' to '#{caller_locations(1, 1)[0].label}' method. " \ + "Valid attributes are: #{COUNTER_ATTRIBUTES.join(', ')}" + end + + return unless available? + + self.fetch # make sure record exists + + attribute = self.connection.quote_column_name(raw_attribute) + + # will be running on its own transaction context + yield(attribute) + end + + # Returns a site statistic record with tracked information + # + # @return [SiteStatistic] record with tracked information + def self.fetch + SiteStatistic.transaction(requires_new: true) do + SiteStatistic.first_or_create! + end + rescue ActiveRecord::RecordNotUnique + retry + end + + # Return whether required schema change is available + # + # This is needed in order to degrade gracefully when testing schema migrations + # + # @return [Boolean] whether schema is available + def self.available? + @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION + end + + # Resets cached column information + # + # This is called during schema migration specs, in order to reset internal cache state + def self.reset_column_information + @available_flag = nil + + super + end +end diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb new file mode 100644 index 00000000000..069d065280e --- /dev/null +++ b/app/policies/concerns/policy_actor.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Include this module if we want to pass something else than the user to +# check policies. This defines several methods which the policy checker +# would call and check. +module PolicyActor + extend ActiveSupport::Concern + + def blocked? + false + end + + def admin? + false + end + + def external? + false + end + + def internal? + false + end + + def access_locked? + false + end + + def required_terms_not_accepted? + false + end + + def can_create_group + false + end +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 4a33160afa1..3205578b83e 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -11,10 +11,15 @@ class PipelineSerializer < BaseSerializer :retryable_builds, :cancelable_statuses, :trigger_requests, - :project, :manual_actions, :artifacts, - { pending_builds: :project } + { + pending_builds: :project, + project: [:route, { namespace: :route }], + artifacts: { + project: [:route, { namespace: :route }] + } + } ]) end diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index 4640c5a2d4b..a1165b0ab28 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -50,17 +50,17 @@ module Clusters end def remove_installation_pod - helm_api.delete_installation_pod!(install_command.pod_name) + helm_api.delete_pod!(install_command.pod_name) rescue # no-op end def installation_phase - helm_api.installation_status(install_command.pod_name) + helm_api.status(install_command.pod_name) end def installation_errors - helm_api.installation_log(install_command.pod_name) + helm_api.log(install_command.pod_name) end end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index a4a66330546..c2a0c5fa7f3 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -77,7 +77,6 @@ module Projects Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) project.old_path_with_namespace = @old_path - project.expires_full_path_cache write_repository_config(@new_path) diff --git a/app/services/prometheus/adapter_service.rb b/app/services/prometheus/adapter_service.rb index cbba79690c5..a791845ba20 100644 --- a/app/services/prometheus/adapter_service.rb +++ b/app/services/prometheus/adapter_service.rb @@ -30,7 +30,7 @@ module Prometheus return unless deployment_platform.respond_to?(:cluster) cluster = deployment_platform.cluster - return unless cluster.application_prometheus&.installed? + return unless cluster.application_prometheus&.ready? cluster.application_prometheus end diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml index 00933d726d9..fdaacc098e0 100644 --- a/app/views/admin/projects/_projects.html.haml +++ b/app/views/admin/projects/_projects.html.haml @@ -17,7 +17,7 @@ - if project.archived %span.badge.badge-warning archived .title - = link_to [:admin, project.namespace.becomes(Namespace), project] do + = link_to(admin_namespace_project_path(project.namespace, project)) do .dash-project-avatar .avatar-container.s40 = project_icon(project, alt: '', class: 'avatar project-avatar s40') diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml index 9f8b0acd763..d29dda43c89 100644 --- a/app/views/ide/index.html.haml +++ b/app/views/ide/index.html.haml @@ -1,6 +1,9 @@ - @body_class = 'ide' - page_title 'IDE' +- content_for :page_specific_javascripts do + = stylesheet_link_tag 'page_bundles/ide' + #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'), "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'), "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'), diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 9253a0652da..ac5916d129c 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -3,6 +3,11 @@ - site_name = "GitLab" %head{ prefix: "og: http://ogp.me/ns#" } %meta{ charset: "utf-8" } + + - if Feature.enabled?('asset_host_prefetch') && ActionController::Base.asset_host + %link{ rel: 'dns-prefetch', href: ActionController::Base.asset_host } + %link{ rel: 'preconnnect', href: ActionController::Base.asset_host, crossorigin: '' } + %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' } -# Open Graph - http://ogp.me/ diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 97c04dda8cb..e8d31992149 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -67,5 +67,5 @@ %button.navbar-toggler.d-block.d-sm-none{ type: 'button' } %span.sr-only= _("Toggle navigation") - = sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right') + = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 0a3b5ec7eea..d471dd84550 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -15,7 +15,7 @@ = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do = link_to group_path(@group) do .nav-icon-container - = sprite_icon('project') + = sprite_icon('home') %span.nav-item-name = _('Overview') diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 94863a3460d..d65f153b451 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -110,7 +110,7 @@ = nav_link(controller: :gpg_keys) do = link_to profile_gpg_keys_path do .nav-icon-container - = sprite_icon('key-2') + = sprite_icon('key-modern') %span.nav-item-name = _('GPG Keys') %ul.sidebar-sub-level-items.is-fly-out-only diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 0ec61df1f0a..2c262a2b7dd 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -11,7 +11,7 @@ = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do = link_to project_path(@project), class: 'shortcuts-project' do .nav-icon-container - = sprite_icon('project') + = sprite_icon('home') %span.nav-item-name = _('Project') @@ -40,7 +40,7 @@ = nav_link(controller: sidebar_repository_paths) do = link_to project_tree_path(@project), class: 'shortcuts-tree' do .nav-icon-container - = sprite_icon('doc_text') + = sprite_icon('doc-text') %span.nav-item-name = _('Repository') diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 290970a1045..af86b8e8e67 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -2,17 +2,11 @@ - page_title "Metrics for environment", @environment.name .prometheus-container{ class: container_class } - #prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'), - "clusters-path": project_clusters_path(@project), - "current-environment-name": @environment.name, - "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), - "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), - "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), - "empty-no-data-svg-path": image_path('illustrations/monitoring/no_data.svg'), - "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'), - "metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json), - "deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json), - "environments-endpoint": project_environments_path(@project, format: :json), - "project-path": project_path(@project), - "tags-path": project_tags_path(@project), - "has-metrics": "#{@environment.has_metrics?}" } } + .top-area + .row + .col-sm-6 + %h3 + Environment: + = link_to @environment.name, environment_path(@environment) + + #prometheus-graphs{ data: metrics_data(@project, @environment) } diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index b88fe47726d..759efd4e9d4 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -86,7 +86,7 @@ - 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 } } - - tooltip = build.tooltip_message + - tooltip = sanitize(build.tooltip_message.dup) = 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}" } 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 ca0f7d6098f..afa7eb06cb4 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -27,7 +27,7 @@ = dropdown_filter(_("Search branches")) = dropdown_content = dropdown_loading - .panel-footer + .card-footer .text-center= icon('spinner spin', class: 'js-source-loading') %ul.list-unstyled.mr_source_commit diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml index 36f56fbad1a..c7f0511d1de 100644 --- a/app/views/shared/snippets/_embed.html.haml +++ b/app/views/shared/snippets/_embed.html.haml @@ -2,7 +2,7 @@ .gitlab-embed-snippets .js-file-title.file-title-flex-parent .file-header-content - = external_snippet_icon('doc_text') + = external_snippet_icon('doc-text') %strong.file-title-name %a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) } diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb index 100d86e38c8..eeeff6e93a0 100644 --- a/app/workers/concerns/gitlab/github_import/object_importer.rb +++ b/app/workers/concerns/gitlab/github_import/object_importer.rb @@ -22,7 +22,7 @@ module Gitlab importer_class.new(object, project, client).execute - counter.increment(project: project.full_path) + counter.increment end def counter diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 5ef9b744db3..68ec66e8499 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -23,9 +23,7 @@ class RepositoryForkWorker def fork_repository(target_project, source_repository_storage_name, source_disk_path) return unless start_fork(target_project) - Gitlab::Metrics.add_event(:fork_repository, - source_path: source_disk_path, - target_path: target_project.disk_path) + Gitlab::Metrics.add_event(:fork_repository) result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path, target_project.repository_storage, target_project.disk_path) diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 25fec542ac7..8c64c513c74 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -11,9 +11,7 @@ class RepositoryImportWorker return unless start_import(project) - Gitlab::Metrics.add_event(:import_repository, - import_url: project.import_url, - path: project.full_path) + Gitlab::Metrics.add_event(:import_repository) service = Projects::ImportService.new(project, project.creator) result = service.execute |