diff options
Diffstat (limited to 'app')
196 files changed, 2290 insertions, 1116 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1f34c6b50c2..464611f66f0 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -9,7 +9,7 @@ const Api = { projectsPath: '/api/:version/projects.json', projectPath: '/api/:version/projects/:id', projectLabelsPath: '/:namespace_path/:project_path/labels', - groupLabelsPath: '/groups/:namespace_path/labels', + groupLabelsPath: '/groups/:namespace_path/-/labels', licensePath: '/api/:version/templates/licenses/:key', gitignorePath: '/api/:version/templates/gitignores/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', @@ -32,7 +32,7 @@ const Api = { }, // Return groups list. Filtered by query - groups(query, options, callback) { + groups(query, options, callback = $.noop) { const url = Api.buildUrl(Api.groupsPath); return axios.get(url, { params: Object.assign({ diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/index.js index 113ab71ec6f..8e31f1865f0 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/index.js @@ -26,7 +26,7 @@ import './components/new_list_dropdown'; import './components/modal/index'; import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first -$(() => { +export default () => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; const ModalStore = gl.issueBoards.ModalStore; @@ -238,4 +238,4 @@ $(() => { </div> `, }); -}); +}; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index da0e8063ccb..ce19069f103 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -7,7 +7,6 @@ mixins: [ pipelinesMixin, ], - props: { endpoint: { type: String, diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue index 39b699a6395..34aa04083e6 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -37,7 +37,7 @@ > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl" /> <h5 class="item-title merge-merquest-title"> <a :href="mergeRequest.url"> {{ mergeRequest.title }} diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js index ca8798facc9..b727261648c 100644 --- a/app/assets/javascripts/deploy_keys/index.js +++ b/app/assets/javascripts/deploy_keys/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import deployKeysApp from './components/app.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: document.getElementById('js-deploy-keys'), components: { deployKeysApp, @@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, }); }, -})); +}); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index 38c42a11b4e..679057e787c 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -71,7 +71,7 @@ export default () => { el: '#resolve-count-app', components: { 'resolve-count': ResolveCount - } + }, }); $(window).trigger('resize.nav'); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 80ee41d3bbb..acf0effa00d 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -42,131 +42,34 @@ var Dispatcher; }); }); - switch (page) { - case 'projects:merge_requests:index': - case 'projects:issues:index': - case 'projects:issues:show': - case 'projects:issues:new': - case 'projects:issues:edit': - case 'projects:merge_requests:creations:new': - case 'projects:merge_requests:creations:diffs': - case 'projects:merge_requests:edit': - case 'projects:merge_requests:show': - case 'projects:commit:show': - case 'projects:activity': - case 'projects:commits:show': - case 'projects:show': - case 'groups:show': - case 'projects:tree:show': - case 'projects:find_file:show': - case 'projects:blob:show': - case 'projects:blame:show': - shortcut_handler = true; - break; - case 'groups:labels:new': - import('./pages/groups/labels/new') - .then(callDefault) - .catch(fail); - break; - case 'groups:labels:edit': - import('./pages/groups/labels/edit') - .then(callDefault) - .catch(fail); - break; - case 'projects:labels:new': - import('./pages/projects/labels/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:labels:edit': - import('./pages/projects/labels/edit') - .then(callDefault) - .catch(fail); - break; - case 'groups:labels:index': - import('./pages/groups/labels/index') - .then(callDefault) - .catch(fail); - break; - case 'projects:labels:index': - import('./pages/projects/labels/index') - .then(callDefault) - .catch(fail); - break; - case 'projects:network:show': - // Ensure we don't create a particular shortcut handler here. This is - // already created, where the network graph is created. - shortcut_handler = true; - break; - case 'projects:forks:new': - import('./pages/projects/forks/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:artifacts:browse': - import('./pages/projects/artifacts/browse') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:artifacts:file': - import('./pages/projects/artifacts/file') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'search:show': - import('./pages/search/show') - .then(callDefault) - .catch(fail); - break; - case 'projects:settings:repository:show': - import('./pages/projects/settings/repository/show') - .then(callDefault) - .catch(fail); - break; - case 'projects:settings:ci_cd:show': - import('./pages/projects/settings/ci_cd/show') - .then(callDefault) - .catch(fail); - break; - case 'groups:settings:ci_cd:show': - import('./pages/groups/settings/ci_cd/show') - .then(callDefault) - .catch(fail); - break; - case 'ci:lints:create': - case 'ci:lints:show': - import('./pages/ci/lints') - .then(callDefault) - .catch(fail); - break; - case 'admin:conversational_development_index:show': - import('./pages/admin/conversational_development_index/show') - .then(callDefault) - .catch(fail); - break; - case 'import:fogbugz:new_user_map': - import('./pages/import/fogbugz/new_user_map') - .then(callDefault) - .catch(fail); - break; - case 'profiles:personal_access_tokens:index': - import('./pages/profiles/personal_access_tokens') - .then(callDefault) - .catch(fail); - break; - case 'admin:impersonation_tokens:index': - import('./pages/admin/impersonation_tokens') - .then(callDefault) - .catch(fail); - break; - case 'dashboard:groups:index': - import('./pages/dashboard/groups/index') - .then(callDefault) - .catch(fail); - break; + const shortcutHandlerPages = [ + 'projects:activity', + 'projects:artifacts:browse', + 'projects:artifacts:file', + 'projects:blame:show', + 'projects:blob:show', + 'projects:commit:show', + 'projects:commits:show', + 'projects:find_file:show', + 'projects:issues:edit', + 'projects:issues:index', + 'projects:issues:new', + 'projects:issues:show', + 'projects:merge_requests:creations:diffs', + 'projects:merge_requests:creations:new', + 'projects:merge_requests:edit', + 'projects:merge_requests:index', + 'projects:merge_requests:show', + 'projects:network:show', + 'projects:show', + 'projects:tree:show', + 'groups:show', + ]; + + if (shortcutHandlerPages.indexOf(page) !== -1) { + shortcut_handler = true; } + switch (path[0]) { case 'admin': switch (path[1]) { diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js deleted file mode 100644 index b693084e434..00000000000 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js +++ /dev/null @@ -1,102 +0,0 @@ -import eventHub from '../event_hub'; -import FilteredSearchTokenizer from '../filtered_search_tokenizer'; - -export default { - name: 'RecentSearchesDropdownContent', - - props: { - items: { - type: Array, - required: true, - }, - isLocalStorageAvailable: { - type: Boolean, - required: false, - default: true, - }, - allowedKeys: { - type: Array, - required: true, - }, - }, - - computed: { - processedItems() { - return this.items.map((item) => { - const { tokens, searchToken } - = FilteredSearchTokenizer.processTokens(item, this.allowedKeys); - - const resultantTokens = tokens.map(token => ({ - prefix: `${token.key}:`, - suffix: `${token.symbol}${token.value}`, - })); - - return { - text: item, - tokens: resultantTokens, - searchToken, - }; - }); - }, - hasItems() { - return this.items.length > 0; - }, - }, - - methods: { - onItemActivated(text) { - eventHub.$emit('recentSearchesItemSelected', text); - }, - onRequestClearRecentSearches(e) { - // Stop the dropdown from closing - e.stopPropagation(); - - eventHub.$emit('requestClearRecentSearches'); - }, - }, - - template: ` - <div> - <div - v-if="!isLocalStorageAvailable" - class="dropdown-info-note"> - This feature requires local storage to be enabled - </div> - <ul v-else-if="hasItems"> - <li - v-for="(item, index) in processedItems" - :key="index"> - <button - type="button" - class="filtered-search-history-dropdown-item" - @click="onItemActivated(item.text)"> - <span> - <span - v-for="(token, tokenIndex) in item.tokens" - class="filtered-search-history-dropdown-token"> - <span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span> - </span> - </span> - <span class="filtered-search-history-dropdown-search-token"> - {{ item.searchToken }} - </span> - </button> - </li> - <li class="divider"></li> - <li> - <button - type="button" - class="filtered-search-history-clear-button" - @click="onRequestClearRecentSearches($event)"> - Clear recent searches - </button> - </li> - </ul> - <div - v-else - class="dropdown-info-note"> - You don't have any recent searches - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue new file mode 100644 index 00000000000..26618af9515 --- /dev/null +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -0,0 +1,104 @@ +<script> +import eventHub from '../event_hub'; +import FilteredSearchTokenizer from '../filtered_search_tokenizer'; + +export default { + name: 'RecentSearchesDropdownContent', + props: { + items: { + type: Array, + required: true, + }, + isLocalStorageAvailable: { + type: Boolean, + required: false, + default: true, + }, + allowedKeys: { + type: Array, + required: true, + }, + }, + computed: { + processedItems() { + return this.items.map((item) => { + const { tokens, searchToken } + = FilteredSearchTokenizer.processTokens(item, this.allowedKeys); + + const resultantTokens = tokens.map(token => ({ + prefix: `${token.key}:`, + suffix: `${token.symbol}${token.value}`, + })); + + return { + text: item, + tokens: resultantTokens, + searchToken, + }; + }); + }, + hasItems() { + return this.items.length > 0; + }, + }, + methods: { + onItemActivated(text) { + eventHub.$emit('recentSearchesItemSelected', text); + }, + onRequestClearRecentSearches(e) { + // Stop the dropdown from closing + e.stopPropagation(); + + eventHub.$emit('requestClearRecentSearches'); + }, + }, +}; +</script> +<template> + <div> + <div + v-if="!isLocalStorageAvailable" + class="dropdown-info-note"> + This feature requires local storage to be enabled + </div> + <ul v-else-if="hasItems"> + <li + v-for="(item, index) in processedItems" + :key="`processed-items-${index}`" + > + <button + type="button" + class="filtered-search-history-dropdown-item" + @click="onItemActivated(item.text)"> + <span> + <span + class="filtered-search-history-dropdown-token" + v-for="(token, index) in item.tokens" + :key="`dropdown-token-${index}`" + > + <span class="name">{{ token.prefix }}</span> + <span class="value">{{ token.suffix }}</span> + </span> + </span> + <span class="filtered-search-history-dropdown-search-token"> + {{ item.searchToken }} + </span> + </button> + </li> + <li class="divider"></li> + <li> + <button + type="button" + class="filtered-search-history-clear-button" + @click="onRequestClearRecentSearches($event)"> + Clear recent searches + </button> + </li> + </ul> + <div + v-else + class="dropdown-info-note"> + You don't have any recent searches + </div> + </div> +</template> diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 22421fc4868..d36f38a70b5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -14,7 +14,6 @@ export default class DropdownUser extends FilteredSearchDropdown { endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, searchKey: 'search', params: { - per_page: 20, active: true, group_id: this.getGroupId(), project_id: this.getProjectId(), diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index cfdd3380fc7..fb4ae1d17dd 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -111,6 +111,9 @@ export default class FilteredSearchDropdown { if (hook) { const data = hook.list.data || []; + + if (!data) return; + const results = data.map((o) => { const updated = o; updated.droplab_hidden = false; diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index c99ed63c4af..f9338b82acf 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content'; +import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content.vue'; import eventHub from './event_hub'; class RecentSearchesRoot { @@ -33,7 +33,7 @@ class RecentSearchesRoot { this.vm = new Vue({ el: this.wrapperElement, components: { - 'recent-searches-dropdown-content': RecentSearchesDropdownContent, + RecentSearchesDropdownContent, }, data() { return state; }, template: ` diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index e322756f256..6cf78bab6ad 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -607,7 +607,20 @@ GitLabDropdown = (function() { }; GitLabDropdown.prototype.renderItem = function(data, group, index) { - var field, fieldName, html, selected, text, url, value; + var field, fieldName, html, selected, text, url, value, rowHidden; + + if (!this.options.renderRow) { + value = this.options.id ? this.options.id(data) : data.id; + + if (value) { + value = value.toString().replace(/'/g, '\\\''); + } + } + + // Hide element + if (this.options.hideRow && this.options.hideRow(value)) { + rowHidden = true; + } if (group == null) { group = false; } @@ -616,6 +629,7 @@ GitLabDropdown = (function() { index = false; } html = document.createElement('li'); + if (data === 'divider' || data === 'separator') { html.className = data; return html; @@ -631,11 +645,9 @@ GitLabDropdown = (function() { html = this.options.renderRow.call(this.options, data, this); } else { if (!selected) { - value = this.options.id ? this.options.id(data) : data.id; fieldName = this.options.fieldName; if (value) { - value = value.toString().replace(/'/g, '\\\''); field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`); if (field.length) { selected = true; diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index 08d0bf6e344..4d86ac8023c 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -30,11 +30,11 @@ default: 'bottom', }, /** - * value could either be number or string - * as `memberCount` is always passed as string - * while `subgroupCount` & `projectCount` - * are always number - */ + * value could either be number or string + * as `memberCount` is always passed as string + * while `subgroupCount` & `projectCount` + * are always number + */ value: { type: [Number, String], required: false, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index dc1930a997f..7151ac05a09 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -213,7 +213,7 @@ export default class LabelsSelect { } } if (label.duplicate) { - color = gl.DropdownUtils.duplicateLabelColor(label.color); + color = DropdownUtils.duplicateLabelColor(label.color); } else { if (label.color != null) { @@ -316,9 +316,9 @@ export default class LabelsSelect { }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(options) { - const { $el, e, isMarking } = options; - const label = options.selectedObj; + clicked: function (clickEvent) { + const { $el, e, isMarking } = clickEvent; + const label = clickEvent.selectedObj; var isIssueIndex, isMRIndex, page, boardsModel; var fadeOutLoader = () => { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 7d2cf4b634f..017f3b986fd 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -418,6 +418,16 @@ export const convertObjectPropsToCamelCase = (obj = {}) => { export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; +export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => { + // Click a .js-select-on-focus field, select the contents + // Prevent a mouseup event from deselecting the input + $(selector).on('focusin', function selectOnFocusCallback() { + $(this).select().one('mouseup', (e) => { + e.preventDefault(); + }); + }); +}; + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index dc9e5bb03f4..659dc9eaa1f 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -10,7 +10,7 @@ window.jQuery = jQuery; window.$ = jQuery; // lib/utils -import { handleLocationHash } from './lib/utils/common_utils'; +import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils'; import { localTimeAgo } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; @@ -104,13 +104,7 @@ document.addEventListener('DOMContentLoaded', () => { return true; }); - // Click a .js-select-on-focus field, select the contents - // Prevent a mouseup event from deselecting the input - $('.js-select-on-focus').on('focusin', function selectOnFocusCallback() { - $(this).select().one('mouseup', (e) => { - e.preventDefault(); - }); - }); + addSelectOnFocusBehaviour('.js-select-on-focus'); $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $(this).tooltip('destroy') diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js index 6e66ef69fe1..c1056537f90 100644 --- a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js +++ b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js @@ -1,3 +1,3 @@ -import UserCallout from '../../../../user_callout'; +import UserCallout from '~/user_callout'; -export default () => new UserCallout(); +document.addEventListener('DOMContentLoaded', () => new UserCallout()); diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js index 030328a1363..78a5c4c27be 100644 --- a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js +++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js @@ -1,3 +1,3 @@ -import DueDateSelectors from '../../../due_date_select'; +import DueDateSelectors from '~/due_date_select'; -export default () => new DueDateSelectors(); +document.addEventListener('DOMContentLoaded', () => new DueDateSelectors()); diff --git a/app/assets/javascripts/pages/ci/lints/create/index.js b/app/assets/javascripts/pages/ci/lints/create/index.js new file mode 100644 index 00000000000..8e8a843da0b --- /dev/null +++ b/app/assets/javascripts/pages/ci/lints/create/index.js @@ -0,0 +1,3 @@ +import CILintEditor from '../ci_lint_editor'; + +document.addEventListener('DOMContentLoaded', () => new CILintEditor()); diff --git a/app/assets/javascripts/pages/ci/lints/index.js b/app/assets/javascripts/pages/ci/lints/index.js deleted file mode 100644 index 5cc66546109..00000000000 --- a/app/assets/javascripts/pages/ci/lints/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import CILintEditor from './ci_lint_editor'; - -export default () => new CILintEditor(); diff --git a/app/assets/javascripts/pages/ci/lints/show/index.js b/app/assets/javascripts/pages/ci/lints/show/index.js new file mode 100644 index 00000000000..8e8a843da0b --- /dev/null +++ b/app/assets/javascripts/pages/ci/lints/show/index.js @@ -0,0 +1,3 @@ +import CILintEditor from '../ci_lint_editor'; + +document.addEventListener('DOMContentLoaded', () => new CILintEditor()); diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js index 9f235ed6a98..79987642796 100644 --- a/app/assets/javascripts/pages/dashboard/groups/index/index.js +++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js @@ -1,3 +1,3 @@ import initGroupsList from '~/groups'; -export default initGroupsList; +document.addEventListener('DOMContentLoaded', initGroupsList); diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js index 72c5e4744ac..fa81ad914ba 100644 --- a/app/assets/javascripts/pages/groups/labels/edit/index.js +++ b/app/assets/javascripts/pages/groups/labels/edit/index.js @@ -1,3 +1,3 @@ import Labels from '~/labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js index 018345fa112..6e45de2a724 100644 --- a/app/assets/javascripts/pages/groups/labels/index/index.js +++ b/app/assets/javascripts/pages/groups/labels/index/index.js @@ -1,3 +1,3 @@ import initLabels from '~/init_labels'; -export default initLabels; +document.addEventListener('DOMContentLoaded', initLabels); diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js index 72c5e4744ac..fa81ad914ba 100644 --- a/app/assets/javascripts/pages/groups/labels/new/index.js +++ b/app/assets/javascripts/pages/groups/labels/new/index.js @@ -1,3 +1,3 @@ import Labels from '~/labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index ad79f7e09ac..04a0d8117cc 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -1,6 +1,6 @@ import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { const variableListEl = document.querySelector('.js-ci-variable-list-section'); // eslint-disable-next-line no-new new AjaxVariableList({ @@ -9,4 +9,4 @@ export default () => { errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); -}; +}); diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js index 5defea104d4..68d4c1f049f 100644 --- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js +++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js @@ -1,3 +1,3 @@ -import UsersSelect from '../../../../users_select'; +import UsersSelect from '~/users_select'; -export default () => new UsersSelect(); +document.addEventListener('DOMContentLoaded', () => new UsersSelect()); diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js index 030328a1363..78a5c4c27be 100644 --- a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js +++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js @@ -1,3 +1,3 @@ -import DueDateSelectors from '../../../due_date_select'; +import DueDateSelectors from '~/due_date_select'; -export default () => new DueDateSelectors(); +document.addEventListener('DOMContentLoaded', () => new DueDateSelectors()); diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js index 02456071086..ea7458fe9b8 100644 --- a/app/assets/javascripts/pages/projects/artifacts/browse/index.js +++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js @@ -1,7 +1,7 @@ import BuildArtifacts from '~/build_artifacts'; import ShortcutsNavigation from '~/shortcuts_navigation'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { new ShortcutsNavigation(); // eslint-disable-line no-new new BuildArtifacts(); // eslint-disable-line no-new -} +}); diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js index 4cd67ac76e3..8484e5e9848 100644 --- a/app/assets/javascripts/pages/projects/artifacts/file/index.js +++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js @@ -1,7 +1,7 @@ import BlobViewer from '~/blob/viewer/index'; import ShortcutsNavigation from '~/shortcuts_navigation'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { new ShortcutsNavigation(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new -} +}); diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js index 3aeeedbb45d..5cfe8723204 100644 --- a/app/assets/javascripts/pages/projects/boards/index.js +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -1,7 +1,9 @@ import UsersSelect from '~/users_select'; import ShortcutsNavigation from '~/shortcuts_navigation'; +import initBoards from '~/boards'; document.addEventListener('DOMContentLoaded', () => { new UsersSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new + initBoards(); }); diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js index 7825eb01949..d80e27e9156 100644 --- a/app/assets/javascripts/pages/projects/forks/new/index.js +++ b/app/assets/javascripts/pages/projects/forks/new/index.js @@ -1,5 +1,3 @@ import ProjectFork from '~/project_fork'; -export default () => { - new ProjectFork(); // eslint-disable-line no-new -}; +document.addEventListener('DOMContentLoaded', () => new ProjectFork()); diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index db064e3f801..1e56aa58da2 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -3,6 +3,7 @@ import Issue from '~/issue'; import ShortcutsIssuable from '~/shortcuts_issuable'; import ZenMode from '~/zen_mode'; import '~/notes/index'; +import '~/issue_show/index'; document.addEventListener('DOMContentLoaded', () => { new Issue(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js index 72c5e4744ac..fa81ad914ba 100644 --- a/app/assets/javascripts/pages/projects/labels/edit/index.js +++ b/app/assets/javascripts/pages/projects/labels/edit/index.js @@ -1,3 +1,3 @@ import Labels from '~/labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 018345fa112..6e45de2a724 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -1,3 +1,3 @@ import initLabels from '~/init_labels'; -export default initLabels; +document.addEventListener('DOMContentLoaded', initLabels); diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js index 72c5e4744ac..fa81ad914ba 100644 --- a/app/assets/javascripts/pages/projects/labels/new/index.js +++ b/app/assets/javascripts/pages/projects/labels/new/index.js @@ -1,3 +1,3 @@ import Labels from '~/labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 6e48d207571..d23ad9a92f4 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -50,7 +50,7 @@ export default class Project { Project.projectSelectDropdown(); } - static projectSelectDropdown () { + static projectSelectDropdown() { projectSelect(); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); } diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index a563d0f9961..6c2a785c0af 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -2,7 +2,7 @@ import initSettingsPanels from '~/settings_panels'; import SecretValues from '~/behaviors/secret_values'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels initSettingsPanels(); @@ -22,4 +22,4 @@ export default function () { errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); -} +}); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index 83b5467fbc0..5a6f4138b10 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -1,3 +1,7 @@ import initSettingsPanels from '~/settings_panels'; +import initDeployKeys from '~/deploy_keys'; -export default initSettingsPanels; +document.addEventListener('DOMContentLoaded', () => { + initDeployKeys(); + initSettingsPanels(); +}); diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js index 4264c5c9dbe..85aaaa2c9da 100644 --- a/app/assets/javascripts/pages/search/show/index.js +++ b/app/assets/javascripts/pages/search/show/index.js @@ -1,3 +1,3 @@ import Search from './search'; -export default () => new Search(); +document.addEventListener('DOMContentLoaded', () => new Search()); 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 e027f08ff5c..7adcf4017b8 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -7,7 +7,6 @@ jobComponent, dropdownJobComponent, }, - props: { title: { type: String, diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 33d441e573e..2ba59051773 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -223,7 +223,8 @@ <div class="table-section section-10 commit-link"> <div class="table-mobile-header" - role="rowheader"> + role="rowheader" + > Status </div> <div class="table-mobile-content"> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 58806aa114a..ecf2b10486e 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -50,9 +50,7 @@ computed: { dropdownClass() { - return this.dropdownContent.length > 0 ? - 'js-builds-dropdown-container' : - 'js-builds-dropdown-loading'; + return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; }, triggerButtonClass() { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 586d188350f..4fd639cce8e 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -73,7 +73,7 @@ export default class ProjectFindFile { // find file } - // files pathes load + // files pathes load load(url) { axios.get(url) .then(({ data }) => { @@ -85,7 +85,7 @@ export default class ProjectFindFile { .catch(() => flash(__('An error occurred while loading filenames'))); } - // render result + // render result renderList(filePaths, searchText) { var blobItemUrl, filePath, html, i, j, len, matches, results; this.element.find(".tree-table > tbody").empty(); diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index f5133111d04..8da37d14f0b 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,3 +1,5 @@ +import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; + let hasUserDefinedProjectPath = false; const deriveProjectPathFromUrl = ($projectImportUrl) => { @@ -36,6 +38,7 @@ const bindEvents = () => { const $changeTemplateBtn = $('.change-template'); const $selectedIcon = $('.selected-icon svg'); const $templateProjectNameInput = $('#template-project-name #project_path'); + const $pushNewProjectTipTrigger = $('.push-new-project-tip'); if ($newProjectForm.length !== 1) { return; @@ -55,6 +58,34 @@ const bindEvents = () => { $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); }); + if ($pushNewProjectTipTrigger) { + $pushNewProjectTipTrigger + .removeAttr('rel') + .removeAttr('target') + .on('click', (e) => { e.preventDefault(); }) + .popover({ + title: $pushNewProjectTipTrigger.data('title'), + placement: 'auto bottom', + html: 'true', + content: $('.push-new-project-tip-template').html(), + }) + .on('shown.bs.popover', () => { + $(document).on('click.popover touchstart.popover', (event) => { + if ($(event.target).closest('.popover').length === 0) { + $pushNewProjectTipTrigger.trigger('click'); + } + }); + + const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus'); + addSelectOnFocusBehaviour(target); + + target.focus(); + }) + .on('hide.bs.popover', () => { + $(document).off('click.popover touchstart.popover'); + }); + } + function chooseTemplate() { $('.template-option').hide(); $projectFieldsForm.addClass('selected'); diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 5482c55f8bb..05a623ca6d9 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -1,6 +1,7 @@ import renderMath from './render_math'; import renderMermaid from './render_mermaid'; import syntaxHighlight from './syntax_highlight'; + // Render Gitlab flavoured Markdown // // Delegates to syntax highlight and render math & mermaid diagrams. diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 689befc742e..14545824e74 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts { super(); this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); - this.editBtn = document.querySelector('.js-issuable-edit'); Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); Mousetrap.bind('r', this.replyWithSelectedText.bind(this)); - Mousetrap.bind('e', this.editIssue.bind(this)); + Mousetrap.bind('e', ShortcutsIssuable.editIssue); if (isMergeRequest) { this.enabledHelp.push('.hidden-shortcut.merge_requests'); @@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts { return false; } - editIssue() { + static editIssue() { // Need to click the element as on issues, editing is inline // on merge request, editing is on a different page - this.editBtn.click(); + document.querySelector('.js-issuable-edit').click(); return false; } diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index 9d22b9d77be..0686910fc7e 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -1,5 +1,5 @@ <script> - import Flash from '../../../flash'; + import Flash from '~/flash'; import editForm from './edit_form.vue'; import issuableMixin from '../../../vue_shared/mixins/issuable'; import Icon from '../../../vue_shared/components/icon.vue'; @@ -53,8 +53,7 @@ discussion_locked: locked, }) .then(() => location.reload()) - .catch(() => Flash(this.__(`Something went wrong trying to - change the locked state of this ${this.issuableDisplayName}`))); + .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`))); }, }, }; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 8958534689c..3385aba0279 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -39,7 +39,6 @@ function UsersSelect(currentUser, els, options = {}) { options.showCurrentUser = $dropdown.data('currentUser'); options.todoFilter = $dropdown.data('todoFilter'); options.todoStateFilter = $dropdown.data('todoStateFilter'); - options.perPage = $dropdown.data('perPage'); showNullUser = $dropdown.data('nullUser'); defaultNullUser = $dropdown.data('nullUserDefault'); showMenuAbove = $dropdown.data('showMenuAbove'); @@ -669,7 +668,6 @@ UsersSelect.prototype.users = function(query, options, callback) { const url = this.buildUrl(this.usersPath); const params = { search: query, - per_page: options.perPage || 20, active: true, project_id: options.projectId || null, group_id: options.groupId || null, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index 7ba6c29006a..162f048aac7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -227,7 +227,8 @@ export default { @click="handleMergeButtonClick()" :disabled="isMergeButtonDisabled" :class="mergeButtonClass" - type="button"> + type="button" + class="qa-merge-button"> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 2968af0d5cb..143fd328d88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -107,10 +107,11 @@ <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> <div class="accept-merge-holder clearfix -js-toggle-container accept-action media space-children"> +js-toggle-container accept-action media space-children" + > <button type="button" - class="btn btn-sm btn-reopen btn-success" + class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" :disabled="isMakingRequest" @click="rebase" > diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index d8f0442ef9d..797f0f6ec0f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -96,9 +96,7 @@ export default { cb.call(null, data); } }) - .catch(() => { - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); + .catch(() => new Flash('Something went wrong. Please try again.')); }, initPolling() { this.pollingInterval = new SmartInterval({ @@ -146,9 +144,7 @@ export default { Project.initRefSwitcher(); } }) - .catch(() => { - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); + .catch(() => new Flash('Something went wrong. Please try again.')); }, handleNotification(data) { if (data.ci_status === this.mr.ciStatus) return; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index ed004b3bb08..9a750ce42bd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -4,7 +4,6 @@ import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; export default class MergeRequestStore { - constructor(data) { this.sha = data.diff_head_sha; this.gitlabLogo = data.gitlabLogo; @@ -169,5 +168,4 @@ export default class MergeRequestStore { return timeagoInstance.format(date); } - } diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 1f72dea1b33..a0cd0cbd200 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -6,12 +6,12 @@ import userAvatarImage from './user_avatar/user_avatar_image.vue'; /** - * Renders header component for job and pipeline page based on UI mockups - * - * Used in: - * - job show page - * - pipeline show page - */ + * Renders header component for job and pipeline page based on UI mockups + * + * Used in: + * - job show page + * - pipeline show page + */ export default { components: { ciIconBadge, @@ -118,7 +118,8 @@ <section class="header-action-buttons" - v-if="actions.length"> + v-if="actions.length" + > <template v-for="(action, i) in actions" > diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 6ae6b179f7f..e832d94d32f 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -1,6 +1,5 @@ <script> /* eslint-disable vue/require-default-prop */ - /* This is a re-usable vue component for rendering a button that will probably be sending off ajax requests and need to show the loading status by setting the `loading` option. diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index f65eab11a27..177d2cfc8da 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -65,7 +65,8 @@ </li> <li class="md-header-tab" - :class="{ active: previewMarkdown }"> + :class="{ active: previewMarkdown }" + > <a class="js-preview-link" href="#md-preview-holder" diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index c44c606a8b2..22fc5757447 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -13,6 +13,12 @@ props: { /** This function will take the information given by the pagination component + + Here is an example `change` method: + + change(pagenum) { + gl.utils.visitUrl(`?page=${pagenum}`); + }, */ change: { type: Function, diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index c4b046a6d68..6b89387ab5f 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -444,6 +444,19 @@ } } +.btn-missing { + color: $notes-light-color; + border: 1px dashed $border-gray-normal-dashed; + border-radius: $border-radius-default; + + &:hover, + &:active, + &:focus { + color: $notes-light-color; + background-color: $white-normal; + } +} + .btn-svg svg { @include btn-svg; } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index a12f28efce6..8604e753c18 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -63,10 +63,6 @@ } } - .project-stats { - display: none; - } - .group-buttons { display: none; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d61809cb0a4..d1d98270ad9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -3,7 +3,6 @@ transition: padding $sidebar-transition-duration; .container-fluid { - background: $white-light; padding: 0 $gl-padding; &.container-blank { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index d0999e60e65..294c59f037f 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -296,7 +296,7 @@ body { line-height: 1.3; font-size: 1.25em; font-weight: $gl-font-weight-bold; - margin: 12px 7px; + margin: 12px 0; } h1, @@ -333,6 +333,10 @@ a > code { font-family: $monospace_font; } +.weight-normal { + font-weight: $gl-font-weight-normal; +} + .commit-sha, .ref-name { @extend .monospace; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 54e13f9d95c..a5a8f6d2206 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -215,8 +215,8 @@ $tooltip-font-size: 12px; */ $gl-padding: 16px; $gl-padding-8: 8px; +$gl-padding-4: 4px; $gl-col-padding: 15px; -$gl-btn-padding: 10px; $gl-input-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top: 10px; @@ -377,6 +377,10 @@ $inactive-badge-background: rgba(0, 0, 0, .08); $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; $btn-white-active: #848484; +$gl-btn-padding: 10px; +$gl-btn-line-height: 16px; +$gl-btn-vert-padding: 8px; +$gl-btn-horz-padding: 12px; /* * Badges diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 6763af4e98b..b9390450477 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -13,10 +13,20 @@ display: inline-block; } + .issuable-meta { + .author_link { + display: inline-block; + } + + .issuable-comments { + height: 18px; + } + } + .icon-merge-request-unmerged { height: 13px; margin-bottom: 3px; - } + } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bf41005b6d5..85de0d8e70f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -678,6 +678,9 @@ a.deploy-project-label { } } +.project-empty-note-panel { + border-bottom: 1px solid $border-color; +} .project-stats { font-size: 0; @@ -686,11 +689,13 @@ a.deploy-project-label { border-bottom: 1px solid $border-color; .nav { - padding-top: 12px; - padding-bottom: 12px; + margin-top: $gl-padding-8; + margin-bottom: $gl-padding-8; > li { display: inline-block; + margin-top: $gl-padding-4; + margin-bottom: $gl-padding-4; &:not(:last-child) { margin-right: $gl-padding; @@ -704,36 +709,32 @@ a.deploy-project-label { float: right; } } + } - > a { - padding: 0; - background-color: transparent; - font-size: 14px; - line-height: 29px; - color: $notes-light-color; + .stat-text, + .stat-link { + padding: $gl-btn-vert-padding 0; + background-color: transparent; + font-size: $gl-font-size; + line-height: $gl-btn-line-height; + color: $notes-light-color; + } - &:hover, - &:focus { - color: $gl-text-color; - text-decoration: underline; - } + .stat-link { + &:hover, + &:focus { + color: $gl-text-color; + text-decoration: underline; } } - } - li.missing { - border: 1px dashed $border-gray-normal-dashed; - border-radius: $border-radius-default; - - a { - padding-left: 10px; - padding-right: 10px; - color: $notes-light-color; - display: block; + .btn { + padding: $gl-btn-vert-padding $gl-btn-horz-padding; + line-height: $gl-btn-line-height; } - &:hover { - background-color: $gray-normal; + .btn-missing { + @extend .btn-missing; } } } @@ -743,7 +744,7 @@ pre.light-well { } .git-empty { - margin: 0 7px 7px; + margin-bottom: 7px; h5 { color: $gl-text-color; @@ -895,6 +896,12 @@ pre.light-well { } } +.project-tip-command { + > .input-group-btn:first-child { + width: auto; + } +} + .protected-branches-list, .protected-tags-list { margin-bottom: 30px; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b04bfaf3e49..e6a41202f04 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -126,10 +126,15 @@ class ApplicationController < ActionController::Base Ability.allowed?(object, action, subject) end - def access_denied! + def access_denied!(message = nil) respond_to do |format| - format.json { head :not_found } - format.any { render "errors/access_denied", layout: "errors", status: 404 } + format.any { head :not_found } + format.html do + render "errors/access_denied", + layout: "errors", + status: 404, + locals: { message: message } + end end end diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index ee23ee0bcc3..352f12a89fd 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -55,7 +55,7 @@ module Boards end def issue - @issue ||= issues_finder.execute.find(params[:id]) + @issue ||= issues_finder.find(params[:id]) end def filter_params diff --git a/app/controllers/concerns/controller_with_cross_project_access_check.rb b/app/controllers/concerns/controller_with_cross_project_access_check.rb new file mode 100644 index 00000000000..a45c3384578 --- /dev/null +++ b/app/controllers/concerns/controller_with_cross_project_access_check.rb @@ -0,0 +1,24 @@ +module ControllerWithCrossProjectAccessCheck + extend ActiveSupport::Concern + + included do + extend Gitlab::CrossProjectAccess::ClassMethods + before_action :cross_project_check + end + + def cross_project_check + if Gitlab::CrossProjectAccess.find_check(self)&.should_run?(self) + authorize_cross_project_page! + end + end + + def authorize_cross_project_page! + return if can?(current_user, :read_cross_project) + + rejection_message = _( + "This page is unavailable because you are not allowed to read information "\ + "across multiple projects." + ) + access_denied!(rejection_message) + end +end diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb index f745deb083c..0931bdf4c04 100644 --- a/app/controllers/concerns/routable_actions.rb +++ b/app/controllers/concerns/routable_actions.rb @@ -3,16 +3,20 @@ module RoutableActions def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) - if routable_authorized?(routable, extra_authorization_proc) ensure_canonical_path(routable, requested_full_path) routable else - route_not_found + handle_not_found_or_authorized(routable) nil end end + # This is overridden in gitlab-ee. + def handle_not_found_or_authorized(_routable) + route_not_found + end + def routable_authorized?(routable, extra_authorization_proc) action = :"read_#{routable.class.to_s.underscore}" return false unless can?(current_user, action, routable) diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 7ad79a1e56c..3dbfabcae8a 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -24,7 +24,7 @@ module UploadsActions # - or redirect to its URL # def show - return render_404 unless uploader.exists? + return render_404 unless uploader&.exists? if uploader.file_storage? disposition = uploader.image_or_video? ? 'inline' : 'attachment' @@ -71,6 +71,9 @@ module UploadsActions def build_uploader_from_params uploader = uploader_class.new(model, secret: params[:secret]) + + return nil unless uploader.model_valid? + uploader.retrieve_from_store!(params[:filename]) uploader end diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb index 9d3d1c23c28..9fb5c525425 100644 --- a/app/controllers/dashboard/application_controller.rb +++ b/app/controllers/dashboard/application_controller.rb @@ -1,6 +1,10 @@ class Dashboard::ApplicationController < ApplicationController + include ControllerWithCrossProjectAccessCheck + layout 'dashboard' + requires_cross_project_access + private def projects diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index 025769f512a..79f563bef86 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,6 +1,8 @@ class Dashboard::GroupsController < Dashboard::ApplicationController include GroupTree + skip_cross_project_access_check :index + def index groups = GroupsFinder.new(current_user, all_available: false).execute render_group_tree(groups) diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index de9f8f9224a..4d4ac025f8c 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController before_action :set_non_archived_param before_action :default_sorting + skip_cross_project_access_check :index, :starred def index @projects = load_projects(params.merge(non_public: true)).page(params[:page]) diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb index 8dd91264451..0ba97e4fd59 100644 --- a/app/controllers/dashboard/snippets_controller.rb +++ b/app/controllers/dashboard/snippets_controller.rb @@ -1,4 +1,6 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController + skip_cross_project_access_check :index + def index @snippets = SnippetsFinder.new( current_user, diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 96ce686c989..4a2bfc1f887 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -1,10 +1,12 @@ class Groups::ApplicationController < ApplicationController include RoutableActions + include ControllerWithCrossProjectAccessCheck layout 'group' skip_before_action :authenticate_user! before_action :group + requires_cross_project_access private diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb index 735915abdaa..cc5ba5878f8 100644 --- a/app/controllers/groups/avatars_controller.rb +++ b/app/controllers/groups/avatars_controller.rb @@ -1,6 +1,8 @@ class Groups::AvatarsController < Groups::ApplicationController before_action :authorize_admin_group! + skip_cross_project_access_check :destroy + def destroy @group.remove_avatar! @group.save diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb index b474f5d15ee..0e8125d6113 100644 --- a/app/controllers/groups/children_controller.rb +++ b/app/controllers/groups/children_controller.rb @@ -1,6 +1,7 @@ module Groups class ChildrenController < Groups::ApplicationController before_action :group + skip_cross_project_access_check :index def index parent = if params[:parent_id].present? diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 21e77431176..2c371e76313 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -6,6 +6,10 @@ class Groups::GroupMembersController < Groups::ApplicationController # Authorize before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] + skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, + :approve_access_request, :leave, :resend_invite, + :override + def index @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 0142ad8278c..4bf6a2a3ad1 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -1,6 +1,7 @@ module Groups module Settings class CiCdController < Groups::ApplicationController + skip_cross_project_access_check :show before_action :authorize_admin_pipeline! def show diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 913e13bf734..cb8771bc97e 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -2,6 +2,8 @@ module Groups class VariablesController < Groups::ApplicationController before_action :authorize_admin_build! + skip_cross_project_access_check :show, :update + def show respond_to do |format| format.json do diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7d129c5dece..14b9d6c22bd 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -19,6 +19,12 @@ class GroupsController < Groups::ApplicationController before_action :user_actions, only: [:show, :subgroups] + skip_cross_project_access_check :index, :new, :create, :edit, :update, + :destroy, :projects + # When loading show as an atom feed, we render events that could leak cross + # project information + skip_cross_project_access_check :show, if: -> { request.format.html? } + layout :determine_layout def index diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 6a21a3f77ad..a1fe02dc852 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,5 +1,6 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::GonHelper + include Gitlab::Allowable include PageLayoutHelper include OauthApplications @@ -8,6 +9,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController before_action :add_gon_variables before_action :load_scopes, only: [:index, :create, :edit] + helper_method :can? + layout 'profile' def index diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index 45c66b63ea5..992c8ea6992 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -34,9 +34,9 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController def target case params[:type]&.downcase when 'issue' - IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) + IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id]) when 'mergerequest' - MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) + MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id]) when 'commit' @project.commit(params[:type_id]) end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 35e67730a27..74c25505e36 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -133,7 +133,7 @@ class Projects::BlobController < Projects::ApplicationController end def after_edit_path - from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) + from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid]) if from_merge_request && @branch_name == @ref diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) + "##{hexdigest(@path)}" diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 0f41af7d87b..6b0b22f8e73 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -40,9 +40,9 @@ class Projects::Clusters::GcpController < Projects::ApplicationController def verify_billing case google_project_billing_status when nil - flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') + flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') when false - flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } + flash.now[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } when true return end diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index a5a2d54ba82..a90030a8312 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -75,7 +75,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap def branch_to @target_project = selected_target_project - if params[:ref].present? + if @target_project && params[:ref].present? @ref = params[:ref] @commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref) end @@ -85,7 +85,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap def update_branches @target_project = selected_target_project - @target_branches = @target_project.repository.branch_names + @target_branches = @target_project ? @target_project.repository.branch_names : [] render layout: false end @@ -121,7 +121,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap @project elsif params[:target_project_id].present? MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project) - .execute.find(params[:target_project_id]) + .find_by(id: params[:target_project_id]) else @project.forked_from_project end diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb index 15e77d854dc..b71f1e5fef4 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController before_action :require_pages_enabled! before_action :authorize_update_pages!, except: [:show] - before_action :domain, only: [:show, :destroy] + before_action :domain, only: [:show, :destroy, :verify] def show end @@ -12,11 +12,23 @@ class Projects::PagesDomainsController < Projects::ApplicationController @domain = @project.pages_domains.new end + def verify + result = VerifyPagesDomainService.new(@domain).execute + + if result[:status] == :success + flash[:notice] = 'Successfully verified domain ownership' + else + flash[:alert] = 'Failed to verify domain ownership' + end + + redirect_to project_pages_domain_path(@project, @domain) + end + def create @domain = @project.pages_domains.create(pages_domain_params) if @domain.valid? - redirect_to project_pages_path(@project) + redirect_to project_pages_domain_path(@project, @domain) else render 'new' end @@ -46,6 +58,6 @@ class Projects::PagesDomainsController < Projects::ApplicationController end def domain - @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s) + @domain ||= @project.pages_domains.find_by!(domain: params[:id].to_s) end end diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb new file mode 100644 index 00000000000..b739d0f0f90 --- /dev/null +++ b/app/controllers/projects/prometheus/metrics_controller.rb @@ -0,0 +1,27 @@ +module Projects + module Prometheus + class MetricsController < Projects::ApplicationController + before_action :authorize_admin_project! + + def active_common + respond_to do |format| + format.json do + matched_metrics = prometheus_service.matched_metrics || {} + + if matched_metrics.any? + render json: matched_metrics + else + head :no_content + end + end + end + end + + private + + def prometheus_service + @prometheus_service ||= project.find_or_initialize_service('prometheus') + end + end + end +end diff --git a/app/controllers/projects/prometheus_controller.rb b/app/controllers/projects/prometheus_controller.rb deleted file mode 100644 index 507468d7102..00000000000 --- a/app/controllers/projects/prometheus_controller.rb +++ /dev/null @@ -1,24 +0,0 @@ -class Projects::PrometheusController < Projects::ApplicationController - before_action :authorize_read_project! - before_action :require_prometheus_metrics! - - def active_metrics - respond_to do |format| - format.json do - matched_metrics = project.prometheus_service.matched_metrics || {} - - if matched_metrics.any? - render json: matched_metrics - else - head :no_content - end - end - end - end - - private - - def require_prometheus_metrics! - render_404 unless project.prometheus_service.present? - end -end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 0370edc6e20..913689a1e74 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -45,7 +45,7 @@ class ProjectsController < Projects::ApplicationController notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } ) else - render 'new' + render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) } end end @@ -114,6 +114,8 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do @notification_setting = current_user.notification_settings_for(@project) if current_user + @project = @project.present(current_user: current_user) + render_landing_page end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index fbad9ba7db8..983f888b8ec 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,9 +1,14 @@ class SearchController < ApplicationController - skip_before_action :authenticate_user! - + include ControllerWithCrossProjectAccessCheck include SearchHelper include RendersCommits + skip_before_action :authenticate_user! + requires_cross_project_access if: -> do + search_term_present = params[:search].present? || params[:term].present? + search_term_present && !params[:project_id].present? + end + layout 'search' def show diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 575ec5c20f0..956df4a0a16 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,15 @@ class UsersController < ApplicationController include RoutableActions include RendersMemberAccess + include ControllerWithCrossProjectAccessCheck + + requires_cross_project_access show: false, + groups: false, + projects: false, + contributed: false, + snippets: true, + calendar: false, + calendar_activities: true skip_before_action :authenticate_user! before_action :user, except: [:exists] @@ -103,12 +112,7 @@ class UsersController < ApplicationController end def load_events - # Get user activity feed for projects common for both users - @events = user.recent_events - .merge(projects_for_current_user) - .references(:project) - .with_associations - .limit_recent(20, params[:offset]) + @events = UserRecentEventsFinder.new(current_user, user, params).execute Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?) end @@ -141,10 +145,6 @@ class UsersController < ApplicationController ).execute.page(params[:page]) end - def projects_for_current_user - ProjectsFinder.new(current_user: current_user).execute - end - def build_canonical_path(user) url_for(params.merge(username: user.to_param)) end diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb index c3f5358b577..e8a03947f59 100644 --- a/app/finders/autocomplete_users_finder.rb +++ b/app/finders/autocomplete_users_finder.rb @@ -1,6 +1,12 @@ class AutocompleteUsersFinder + # The number of users to display in the results is hardcoded to 20, and + # pagination is not supported. This ensures that performance remains + # consistent and removes the need for implementing keyset pagination to ensure + # good performance. + LIMIT = 20 + attr_reader :current_user, :project, :group, :search, :skip_users, - :page, :per_page, :author_id, :params + :author_id, :params def initialize(params:, current_user:, project:, group:) @current_user = current_user @@ -8,8 +14,6 @@ class AutocompleteUsersFinder @group = group @search = params[:search] @skip_users = params[:skip_users] - @page = params[:page] - @per_page = params[:per_page] @author_id = params[:author_id] @params = params end @@ -20,7 +24,7 @@ class AutocompleteUsersFinder items = items.reorder(:name) items = items.search(search) if search.present? items = items.where.not(id: skip_users) if skip_users.present? - items = items.page(page).per(per_page) + items = items.limit(LIMIT) if params[:todo_filter].present? && current_user items = items.todo_authors(current_user.id, params[:todo_state_filter]) @@ -52,9 +56,13 @@ class AutocompleteUsersFinder end def users_from_project - user_ids = project.team.users.pluck(:id) - user_ids << author_id if author_id.present? + if author_id.present? + union = Gitlab::SQL::Union + .new([project.authorized_users, User.where(id: author_id)]) - User.where(id: user_ids) + User.from("(#{union.to_sql}) #{User.table_name}") + else + project.authorized_users + end end end diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb new file mode 100644 index 00000000000..2e905fa5750 --- /dev/null +++ b/app/finders/concerns/finder_methods.rb @@ -0,0 +1,51 @@ +module FinderMethods + def find_by!(*args) + raise_not_found_unless_authorized execute.find_by!(*args) + end + + def find_by(*args) + if_authorized execute.find_by(*args) + end + + def find(*args) + raise_not_found_unless_authorized model.find(*args) + end + + private + + def raise_not_found_unless_authorized(result) + result = if_authorized(result) + + raise ActiveRecord::RecordNotFound.new("Couldn't find #{model}") unless result + + result + end + + def if_authorized(result) + # Return the result if the finder does not perform authorization checks. + # this is currently the case in the `MilestoneFinder` + return result unless respond_to?(:current_user) + + if can_read_object?(result) + result + else + nil + end + end + + def can_read_object?(object) + # When there's no policy, we'll allow the read, this is for example the case + # for Todos + return true unless DeclarativePolicy.has_policy?(object) + + model_name = object&.model_name || model.model_name + + Ability.allowed?(current_user, :"read_#{model_name.singular}", object) + end + + # This fetches the model from the `ActiveRecord::Relation` but does not + # actually execute the query. + def model + execute.model + end +end diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb new file mode 100644 index 00000000000..92bf98d7cd2 --- /dev/null +++ b/app/finders/concerns/finder_with_cross_project_access.rb @@ -0,0 +1,70 @@ +# Module to prepend into finders to specify wether or not the finder requires +# cross project access +# +# This module depends on the finder implementing the following methods: +# +# - `#execute` should return an `ActiveRecord::Relation` +# - `#current_user` the user that requires access (or nil) +module FinderWithCrossProjectAccess + extend ActiveSupport::Concern + extend ::Gitlab::Utils::Override + + prepended do + extend Gitlab::CrossProjectAccess::ClassMethods + end + + override :execute + def execute(*args) + check = Gitlab::CrossProjectAccess.find_check(self) + original = super + + return original unless check + return original if should_skip_cross_project_check || can_read_cross_project? + + if check.should_run?(self) + original.model.none + else + original + end + end + + # We can skip the cross project check for finding indivitual records. + # this would be handled by the `can?(:read_*, result)` call in `FinderMethods` + # itself. + override :find_by! + def find_by!(*args) + skip_cross_project_check { super } + end + + override :find_by + def find_by(*args) + skip_cross_project_check { super } + end + + override :find + def find(*args) + skip_cross_project_check { super } + end + + private + + attr_accessor :should_skip_cross_project_check + + def skip_cross_project_check + self.should_skip_cross_project_check = true + + yield + ensure + # The find could raise an `ActiveRecord::RecordNotFound`, after which we + # still want to re-enable the check. + self.should_skip_cross_project_check = false + end + + def can_read_cross_project? + Ability.allowed?(current_user, :read_cross_project) + end + + def can_read_project?(project) + Ability.allowed?(current_user, :read_project, project) + end +end diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index 46ecbaba73a..8676925a540 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -1,6 +1,10 @@ class EventsFinder + prepend FinderMethods + prepend FinderWithCrossProjectAccess attr_reader :source, :params, :current_user + requires_cross_project_access unless: -> { source.is_a?(Project) } + # Used to filter Events # # Arguments: diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 384a336e2bb..9dd6634b38f 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -21,8 +21,12 @@ # my_reaction_emoji: string # class IssuableFinder + prepend FinderWithCrossProjectAccess + include FinderMethods include CreatedAtFilter + requires_cross_project_access unless: -> { project? } + NONE = '0'.freeze attr_accessor :current_user, :params @@ -87,14 +91,6 @@ class IssuableFinder by_my_reaction_emoji(items) end - def find(*params) - execute.find(*params) - end - - def find_by(*params) - execute.find_by(*params) - end - def row_count Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state]) end @@ -124,10 +120,6 @@ class IssuableFinder counts end - def find_by!(*params) - execute.find_by!(*params) - end - def group return @group if defined?(@group) diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 1427cdaa382..5c9fce211ec 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -1,6 +1,10 @@ class LabelsFinder < UnionFinder + prepend FinderWithCrossProjectAccess + include FinderMethods include Gitlab::Utils::StrongMemoize + requires_cross_project_access unless: -> { project? } + def initialize(current_user, params = {}) @current_user = current_user @params = params @@ -35,7 +39,7 @@ class LabelsFinder < UnionFinder end end elsif only_group_labels? - label_ids << Label.where(group_id: group.id) + label_ids << Label.where(group_id: group_ids) else label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(project_id: projects.select(:id)) @@ -55,10 +59,11 @@ class LabelsFinder < UnionFinder items.where(title: title) end - def group - strong_memoize(:group) do + def group_ids + strong_memoize(:group_ids) do group = Group.find(params[:group_id]) - authorized_to_read_labels?(group) && group + groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group] + groups_user_can_read_labels(groups).map(&:id) end end @@ -116,4 +121,10 @@ class LabelsFinder < UnionFinder Ability.allowed?(current_user, :read_label, label_parent) end + + def groups_user_can_read_labels(groups) + DeclarativePolicy.user_scope do + groups.select { |group| authorized_to_read_labels?(group) } + end + end end diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb index 189eb3847eb..f358938344e 100644 --- a/app/finders/merge_request_target_project_finder.rb +++ b/app/finders/merge_request_target_project_finder.rb @@ -1,4 +1,6 @@ class MergeRequestTargetProjectFinder + include FinderMethods + attr_reader :current_user, :source_project def initialize(current_user: nil, source_project:) diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index b4605fca193..f5d2b9f253a 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -8,6 +8,8 @@ # state - filters by state. class MilestonesFinder + include FinderMethods + attr_reader :params, :project_ids, :group_ids def initialize(params = {}) diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index ec61fe1892e..a73c573736e 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -13,7 +13,9 @@ # params are optional class SnippetsFinder < UnionFinder include Gitlab::Allowable - attr_accessor :current_user, :params, :project + include FinderMethods + + attr_accessor :current_user, :project, :params def initialize(current_user, params = {}) @current_user = current_user @@ -52,10 +54,14 @@ class SnippetsFinder < UnionFinder end def authorized_snippets - Snippet.where(feature_available_projects.or(not_project_related)).public_or_visible_to_user(current_user) + Snippet.where(feature_available_projects.or(not_project_related)) + .public_or_visible_to_user(current_user) end def feature_available_projects + # Don't return any project related snippets if the user cannot read cross project + return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) + projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part| part.with_feature_available_for_user(:snippets, current_user) end.select(:id) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 3502bf08971..edb17843002 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -13,6 +13,11 @@ # class TodosFinder + prepend FinderWithCrossProjectAccess + include FinderMethods + + requires_cross_project_access unless: -> { project? } + NONE = '0'.freeze attr_accessor :current_user, :params diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb new file mode 100644 index 00000000000..6f7f7c30d92 --- /dev/null +++ b/app/finders/user_recent_events_finder.rb @@ -0,0 +1,33 @@ +# Get user activity feed for projects common for a user and a logged in user +# +# - current_user: The user viewing the events +# - user: The user for which to load the events +# - params: +# - offset: The page of events to return +class UserRecentEventsFinder + prepend FinderWithCrossProjectAccess + include FinderMethods + + requires_cross_project_access + + attr_reader :current_user, :target_user, :params + + def initialize(current_user, target_user, params = {}) + @current_user = current_user + @target_user = target_user + @params = params + end + + def execute + target_user + .recent_events + .merge(projects_for_current_user) + .references(:project) + .with_associations + .limit_recent(20, params[:offset]) + end + + def projects_for_current_user + ProjectsFinder.new(current_user: current_user).execute + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a6011eb9f30..475341cf9b1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -34,7 +34,7 @@ module ApplicationHelper def project_icon(project_id, options = {}) project = - if project_id.is_a?(Project) + if project_id.respond_to?(:avatar_url) project_id else Project.find_by_full_path(project_id) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index e293b3ef329..ab68ecad2ba 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -199,6 +199,7 @@ module ApplicationSettingsHelper :metrics_port, :metrics_sample_interval, :metrics_timeout, + :pages_domain_verification_enabled, :password_authentication_enabled_for_web, :password_authentication_enabled_for_git, :performance_bar_allowed_group_id, diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index a6e1de6ffdc..0e806d16bc5 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -12,75 +12,42 @@ module BlobHelper def edit_blob_path(project = @project, ref = @ref, path = @path, options = {}) project_edit_blob_path(project, - tree_join(ref, path), - options[:link_opts]) - end - - def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) - blob = options.delete(:blob) - blob ||= project.repository.blob_at(ref, path) rescue nil - - return unless blob && blob.readable_text? - - common_classes = "btn js-edit-blob #{options[:extra_class]}" - - if !on_top_of_branch?(project, ref) - button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } - # This condition applies to anonymous or users who can edit directly - elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) - link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm" - elsif current_user && can?(current_user, :fork_project, project) - continue_params = { - to: edit_blob_path(project, ref, path, options), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag 'Edit', - class: "#{common_classes} js-edit-blob-link-fork-toggler", - data: { action: 'edit', fork_path: fork_path } - end + tree_join(ref, path), + options[:link_opts]) end def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}" end - def ide_edit_text - "#{_('Web IDE')}" - end + def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) + return unless blob = readable_blob(options, path, project, ref) - def ide_blob_link(project = @project, ref = @ref, path = @path, options = {}) - return unless show_new_ide? + common_classes = "btn js-edit-blob #{options[:extra_class]}" - blob = options.delete(:blob) - blob ||= project.repository.blob_at(ref, path) rescue nil + edit_button_tag(blob, + common_classes, + _('Edit'), + edit_blob_path(project, ref, path, options), + project, + ref) + end - return unless blob && blob.readable_text? + def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) + return unless show_new_ide? + return unless blob = readable_blob(options, path, project, ref) common_classes = "btn js-edit-ide #{options[:extra_class]}" - if !on_top_of_branch?(project, ref) - button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' } - # This condition applies to anonymous or users who can edit directly - elsif current_user && can_modify_blob?(blob, project, ref) - link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" - elsif current_user && can?(current_user, :fork_project, project) - continue_params = { - to: ide_edit_path(project, ref, path, options), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag ide_edit_text, - class: common_classes, - data: { fork_path: fork_path } - end + edit_button_tag(blob, + common_classes, + _('Web IDE'), + ide_edit_path(project, ref, path, options), + project, + ref) end - def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) + def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) return unless current_user blob = project.repository.blob_at(ref, path) rescue nil @@ -96,21 +63,12 @@ module BlobHelper elsif can_modify_blob?(blob, project, ref) button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' elsif can?(current_user, :fork_project, project) - continue_params = { - to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to #{action} this file again.", - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag label, - class: "#{common_classes} js-edit-blob-link-fork-toggler", - data: { action: action, fork_path: fork_path } + edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action) end end def replace_blob_link(project = @project, ref = @ref, path = @path) - modify_file_link( + modify_file_button( project, ref, path, @@ -122,7 +80,7 @@ module BlobHelper end def delete_blob_link(project = @project, ref = @ref, path = @path) - modify_file_link( + modify_file_button( project, ref, path, @@ -332,4 +290,55 @@ module BlobHelper options end + + def readable_blob(options, path, project, ref) + blob = options.delete(:blob) + blob ||= project.repository.blob_at(ref, path) rescue nil + + blob if blob&.readable_text? + end + + def edit_blob_fork_params(path) + { + to: path, + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now + } + end + + def edit_modify_file_fork_params(action) + { + to: request.fullpath, + notice: edit_in_new_fork_notice_action(action), + notice_now: edit_in_new_fork_notice_now + } + end + + def edit_fork_button_tag(common_classes, project, label, params, action = 'edit') + fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: params) + + button_tag label, + class: "#{common_classes} js-edit-blob-link-fork-toggler", + data: { action: action, fork_path: fork_path } + end + + def edit_disabled_button_tag(button_text, common_classes) + button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }) + end + + def edit_link_tag(link_text, edit_path, common_classes) + link_to link_text, edit_path, class: "#{common_classes} btn-sm" + end + + def edit_button_tag(blob, common_classes, text, edit_path, project, ref) + if !on_top_of_branch?(project, ref) + edit_disabled_button_tag(text, common_classes) + # This condition only applies to users who are logged in + # Web IDE (Beta) requires the user to have this feature enabled + elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) + edit_link_tag(text, edit_path, common_classes) + elsif current_user && can?(current_user, :fork_project, project) + edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path)) + end + end end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 2641a98e29e..00b9a0e00eb 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -10,12 +10,6 @@ module BranchesHelper project_branches_path(@project, @id, options) end - def can_push_branch?(project, branch_name) - return false unless project.repository.branch_exists?(branch_name) - - ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch_name) - end - def project_branches options_for_select(@project.repository.branch_names, @project.default_branch) end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index c25b54eadc6..19aa55a8d49 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -6,4 +6,28 @@ module DashboardHelper def assigned_mrs_dashboard_path merge_requests_dashboard_path(assignee_id: current_user.id) end + + def dashboard_nav_links + @dashboard_nav_links ||= get_dashboard_nav_links + end + + def dashboard_nav_link?(link) + dashboard_nav_links.include?(link) + end + + def any_dashboard_nav_link?(links) + links.any? { |link| dashboard_nav_link?(link) } + end + + private + + def get_dashboard_nav_links + links = [:projects, :groups, :snippets] + + if can?(current_user, :read_cross_project) + links += [:activity, :milestones] + end + + links + end end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index b981a1e8242..f062a91a166 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -25,8 +25,24 @@ module ExploreHelper controller.class.name.split("::").first == "Explore" end + def explore_nav_links + @explore_nav_links ||= get_explore_nav_links + end + + def explore_nav_link?(link) + explore_nav_links.include?(link) + end + + def any_explore_nav_link?(links) + links.any? { |link| explore_nav_link?(link) } + end + private + def get_explore_nav_links + [:projects, :groups, :snippets] + end + def request_path_with_options(options = {}) request.path + "?#{options.to_param}" end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 23de3590b93..5fbaa17c40e 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -3,6 +3,14 @@ module GroupsHelper %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] end + def group_sidebar_links + @group_sidebar_links ||= get_group_sidebar_links + end + + def group_sidebar_link?(link) + group_sidebar_links.include?(link) + end + def can_change_group_visibility_level?(group) can?(current_user, :change_visibility_level, group) end @@ -107,6 +115,20 @@ module GroupsHelper private + def get_group_sidebar_links + links = [:overview, :group_members] + + if can?(current_user, :read_cross_project) + links += [:activity, :issues, :labels, :milestones, :merge_requests] + end + + if can?(current_user, :admin_group, @group) + links << :settings + end + + links + end + def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false) link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do output = diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 7cd84fe69c9..44ecc2212f2 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -234,7 +234,7 @@ module IssuablesHelper data.merge!(updated_at_by(issuable)) - data.to_json + data end def updated_at_by(issuable) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 64cd3032780..0f25d401406 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -47,27 +47,6 @@ module IssuesHelper end end - def milestone_options(object) - milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a - milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed? - milestones.unshift(Milestone::None) - - options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) - end - - def project_options(issuable, current_user, ability: :read_project) - projects = current_user.authorized_projects.order_id_desc - projects = projects.select do |project| - current_user.can?(ability, project) - end - - no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project') - projects.unshift(no_project) - projects.delete(issuable.project) - - options_from_collection_for_select(projects, :id, :name_with_namespace) - end - def status_box_class(item) if item.try(:expired?) 'status-box-expired' diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 680ea96a556..56c88e6eab0 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,4 +1,12 @@ module NavHelper + def header_links + @header_links ||= get_header_links + end + + def header_link?(link) + header_links.include?(link) + end + def page_with_sidebar_class class_name = page_gutter_class class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar @@ -38,4 +46,28 @@ module NavHelper class_names end + + private + + def get_header_links + links = if current_user + [:user_dropdown] + else + [:sign_in] + end + + if can?(current_user, :read_cross_project) + links += [:issues, :merge_requests, :todos] if current_user.present? + end + + if @project&.persisted? || can?(current_user, :read_cross_project) + links << :search + end + + if session[:impersonator_id] + links << :admin_impersonation + end + + links + end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index aaee6eaeedd..373dfd457f7 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -48,30 +48,4 @@ module PreferencesHelper def user_color_scheme Gitlab::ColorSchemes.for_user(current_user).css_class end - - def default_project_view - return anonymous_project_view unless current_user - - user_view = current_user.project_view - - if can?(current_user, :download_code, @project) - user_view - elsif user_view == "activity" - "activity" - elsif can?(current_user, :read_wiki, @project) - "wiki" - elsif @project.feature_available?(:issues, current_user) - "projects/issues/issues" - else - "customize_workflow" - end - end - - def anonymous_project_view - if !@project.empty_repo? && can?(current_user, :download_code, @project) - 'files' - else - 'activity' - end - end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b97b72d62c3..cc1c69a1999 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -153,11 +153,6 @@ module ProjectsHelper end end - def license_short_name(project) - license = project.repository.license - license&.nickname || license&.name || 'LICENSE' - end - def last_push_event current_user&.recent_push(@project) end @@ -213,6 +208,7 @@ module ProjectsHelper controller.controller_name, controller.action_name, Gitlab::CurrentSettings.cache_key, + "cross-project:#{can?(current_user, :read_cross_project)}", 'v2.5' ] @@ -265,6 +261,17 @@ module ProjectsHelper !!(params[:personal] || params[:name] || any_projects?(projects)) end + def push_to_create_project_command(user = current_user) + repository_url = + if Gitlab::CurrentSettings.current_application_settings.enabled_git_access_protocol == 'http' + user_url(user) + else + Gitlab.config.gitlab_shell.ssh_path_prefix + user.username + end + + "git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)" + end + private def repo_children_classes(field) @@ -390,55 +397,6 @@ module ProjectsHelper end end - def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil) - commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name } - project_new_blob_path( - project, - project.default_branch || 'master', - file_name: file_name, - commit_message: commit_message, - branch_name: branch_name, - context: context - ) - end - - def add_koding_stack_path(project) - project_new_blob_path( - project, - project.default_branch || 'master', - file_name: '.koding.yml', - commit_message: "Add Koding stack script", - content: <<-CONTENT.strip_heredoc - provider: - aws: - access_key: '${var.aws_access_key}' - secret_key: '${var.aws_secret_key}' - resource: - aws_instance: - #{project.path}-vm: - instance_type: t2.nano - user_data: |- - - # Created by GitLab UI for :> - - echo _KD_NOTIFY_@Installing Base packages...@ - - apt-get update -y - apt-get install git -y - - echo _KD_NOTIFY_@Cloning #{project.name}...@ - - export KODING_USER=${var.koding_user_username} - export REPO_URL=#{root_url}${var.koding_queryString_repo}.git - export BRANCH=${var.koding_queryString_branch} - - sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH - - echo _KD_NOTIFY_@#{project.name} cloned.@ - CONTENT - ) - end - def koding_project_url(project = nil, branch = nil, sha = nil) if project import_path = "/Home/Stacks/import" @@ -455,36 +413,6 @@ module ProjectsHelper Gitlab::CurrentSettings.koding_url end - def contribution_guide_path(project) - if project && contribution_guide = project.repository.contribution_guide - project_blob_path( - project, - tree_join(project.default_branch, - contribution_guide.name) - ) - end - end - - def readme_path(project) - filename_path(project, :readme) - end - - def changelog_path(project) - filename_path(project, :changelog) - end - - def license_path(project) - filename_path(project, :license_blob) - end - - def version_path(project) - filename_path(project, :version) - end - - def ci_configuration_path(project) - filename_path(project, :gitlab_ci_yml) - end - def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } project_wiki_path(proj, page, url_params) @@ -510,15 +438,6 @@ module ProjectsHelper @ref || @repository.try(:root_ref) end - def filename_path(project, filename) - if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend - project_blob_path( - project, - tree_join(project.default_branch, blob.name) - ) - end - end - def sanitize_repo_path(project, message) return '' unless message.present? @@ -608,4 +527,8 @@ module ProjectsHelper project_find_file_path(@project, ref) end + + def can_show_last_commit_in_list?(project) + can?(current_user, :read_cross_project) && project.commit + end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index d39cac0f510..f6a6d9bebde 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -55,7 +55,9 @@ module TreeHelper def tree_edit_branch(project = @project, ref = @ref) return unless can_edit_tree?(project, ref) - if can_push_branch?(project, ref) + project = project.present(current_user: current_user) + + if project.can_current_user_push_to_branch?(ref) ref else project = tree_edit_project(project) @@ -81,6 +83,10 @@ module TreeHelper " A fork of this project has been created that you can make changes in, so you can submit a merge request." end + def edit_in_new_fork_notice_action(action) + edit_in_new_fork_notice + " Try to #{action} this file again." + end + def commit_in_fork_help "A new branch will be created in your fork and a new merge request will be started." end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index b5f54d3e154..01af68088df 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -14,4 +14,18 @@ module UsersHelper content_tag(:strong) { user.unconfirmed_email } + h('.') + content_tag(:p) { confirmation_link } end + + def profile_tabs + @profile_tabs ||= get_profile_tabs + end + + def profile_tab?(tab) + profile_tabs.include?(tab) + end + + private + + def get_profile_tabs + [:activity, :groups, :contributed, :projects, :snippets] + end end diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb new file mode 100644 index 00000000000..0027dfdc36b --- /dev/null +++ b/app/mailers/emails/pages_domains.rb @@ -0,0 +1,43 @@ +module Emails + module PagesDomains + def pages_domain_enabled_email(domain, recipient) + @domain = domain + @project = domain.project + + mail( + to: recipient.notification_email, + subject: subject("GitLab Pages domain '#{domain.domain}' has been enabled") + ) + end + + def pages_domain_disabled_email(domain, recipient) + @domain = domain + @project = domain.project + + mail( + to: recipient.notification_email, + subject: subject("GitLab Pages domain '#{domain.domain}' has been disabled") + ) + end + + def pages_domain_verification_succeeded_email(domain, recipient) + @domain = domain + @project = domain.project + + mail( + to: recipient.notification_email, + subject: subject("Verification succeeded for GitLab Pages domain '#{domain.domain}'") + ) + end + + def pages_domain_verification_failed_email(domain, recipient) + @domain = domain + @project = domain.project + + mail( + to: recipient.notification_email, + subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'") + ) + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index eade0fe278f..45d4fb451d8 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -5,6 +5,7 @@ class Notify < BaseMailer include Emails::Issues include Emails::MergeRequests include Emails::Notes + include Emails::PagesDomains include Emails::Projects include Emails::Profile include Emails::Pipelines diff --git a/app/models/ability.rb b/app/models/ability.rb index 0b6bcbde5d9..6dae49f38dc 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -22,12 +22,30 @@ class Ability # # issues - The issues to reduce down to those readable by the user. # user - The User for which to check the issues - def issues_readable_by_user(issues, user = nil) + # filters - A hash of abilities and filters to apply if the user lacks this + # ability + def issues_readable_by_user(issues, user = nil, filters: {}) + issues = apply_filters_if_needed(issues, user, filters) + DeclarativePolicy.user_scope do issues.select { |issue| issue.visible_to_user?(user) } end end + # Returns an Array of MergeRequests that can be read by the given user. + # + # merge_requests - MRs out of which to collect mr's readable by the user. + # user - The User for which to check the merge_requests + # filters - A hash of abilities and filters to apply if the user lacks this + # ability + def merge_requests_readable_by_user(merge_requests, user = nil, filters: {}) + merge_requests = apply_filters_if_needed(merge_requests, user, filters) + + DeclarativePolicy.user_scope do + merge_requests.select { |mr| allowed?(user, :read_merge_request, mr) } + end + end + def can_edit_note?(user, note) allowed?(user, :edit_note, note) end @@ -53,5 +71,15 @@ class Ability cache = RequestStore.active? ? RequestStore : {} DeclarativePolicy.policy_for(user, subject, cache: cache) end + + private + + def apply_filters_if_needed(elements, user, filters) + filters.each do |ability, filter| + elements = filter.call(elements) unless allowed?(user, ability) + end + + elements + end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ee987949080..b230b7f47ef 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -467,7 +467,7 @@ module Ci if cache && project.jobs_cache_index cache = cache.merge( - key: "#{cache[:key]}_#{project.jobs_cache_index}") + key: "#{cache[:key]}-#{project.jobs_cache_index}") end [cache] diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index 80c9f7d4eb4..bfda5b1678b 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -35,6 +35,7 @@ module ProtectedRefAccess def check_access(user) return true if user.admin? - project.team.max_member_access(user.id) >= access_level + user.can?(:push_code, project) && + project.team.max_member_access(user.id) >= access_level end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 93628b456f2..c81f7e52bb1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -159,7 +159,18 @@ class Issue < ActiveRecord::Base object.all_references(current_user, extractor: ext) end - ext.merge_requests.sort_by(&:iid) + merge_requests = ext.merge_requests.sort_by(&:iid) + + cross_project_filter = -> (merge_requests) do + merge_requests.select { |mr| mr.target_project == project } + end + + Ability.merge_requests_readable_by_user( + merge_requests, current_user, + filters: { + read_cross_project: cross_project_filter + } + ) end # All branches containing the current issue's ID, except for diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 472b348a545..fd70e920c7e 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -85,6 +85,7 @@ class NotificationRecipient return false unless user.can?(:receive_notifications) return true if @skip_read_ability + return false if @target && !user.can?(:read_cross_project) return false if @project && !user.can?(:read_project, @project) return true unless read_ability diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index d8bf54e0c40..588bd50ed77 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -1,10 +1,14 @@ class PagesDomain < ActiveRecord::Base + VERIFICATION_KEY = 'gitlab-pages-verification-code'.freeze + VERIFICATION_THRESHOLD = 3.days.freeze + belongs_to :project validates :domain, hostname: { allow_numeric_hostname: true } validates :domain, uniqueness: { case_sensitive: false } validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :key, certificate_key: true, allow_nil: true, allow_blank: true + validates :verification_code, presence: true, allow_blank: false validate :validate_pages_domain validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? } @@ -16,10 +20,32 @@ class PagesDomain < ActiveRecord::Base key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' + after_initialize :set_verification_code after_create :update_daemon - after_save :update_daemon + after_update :update_daemon, if: :pages_config_changed? after_destroy :update_daemon + scope :enabled, -> { where('enabled_until >= ?', Time.now ) } + scope :needs_verification, -> do + verified_at = arel_table[:verified_at] + enabled_until = arel_table[:enabled_until] + threshold = Time.now + VERIFICATION_THRESHOLD + + where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold)))) + end + + def verified? + !!verified_at + end + + def unverified? + !verified? + end + + def enabled? + !Gitlab::CurrentSettings.pages_domain_verification_enabled? || enabled_until.present? + end + def to_param domain end @@ -84,12 +110,49 @@ class PagesDomain < ActiveRecord::Base @certificate_text ||= x509.try(:to_text) end + # Verification codes may be TXT records for domain or verification_domain, to + # support the use of CNAME records on domain. + def verification_domain + return unless domain.present? + + "_#{VERIFICATION_KEY}.#{domain}" + end + + def keyed_verification_code + return unless verification_code.present? + + "#{VERIFICATION_KEY}=#{verification_code}" + end + private + def set_verification_code + return if self.verification_code.present? + + self.verification_code = SecureRandom.hex(16) + end + def update_daemon ::Projects::UpdatePagesConfigurationService.new(project).execute end + def pages_config_changed? + project_id_changed? || + domain_changed? || + certificate_changed? || + key_changed? || + became_enabled? || + became_disabled? + end + + def became_enabled? + enabled_until.present? && !enabled_until_was.present? + end + + def became_disabled? + !enabled_until.present? && enabled_until_was.present? + end + def validate_matching_key unless has_matching_key? self.errors.add(:key, "doesn't match the certificate") diff --git a/app/models/project.rb b/app/models/project.rb index 79058d51af8..ba278a49688 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -15,6 +15,7 @@ class Project < ActiveRecord::Base include ValidAttribute include ProjectFeaturesCompatibility include SelectForProjectAuthorization + include Presentable include Routable include GroupDescendant include Gitlab::SQL::Pattern @@ -1036,6 +1037,9 @@ class Project < ActiveRecord::Base end def user_can_push_to_empty_repo?(user) + return false unless empty_repo? + return false unless Ability.allowed?(user, :push_code, self) + !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 1bb576ff971..58731451429 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -69,16 +69,16 @@ class PrometheusService < MonitoringService client.ping { success: true, result: 'Checked API endpoint' } - rescue Gitlab::PrometheusError => err + rescue Gitlab::PrometheusClient::Error => err { success: false, result: err } end def environment_metrics(environment) - with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &method(:rename_data_to_metrics)) + with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &rename_field(:data, :metrics)) end def deployment_metrics(deployment) - metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &method(:rename_data_to_metrics)) + metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &rename_field(:data, :metrics)) metrics&.merge(deployment_time: deployment.created_at.to_i) || {} end @@ -107,7 +107,7 @@ class PrometheusService < MonitoringService data: data, last_update: Time.now.utc } - rescue Gitlab::PrometheusError => err + rescue Gitlab::PrometheusClient::Error => err { success: false, result: err.message } end @@ -116,10 +116,10 @@ class PrometheusService < MonitoringService Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url)) else cluster = cluster_with_prometheus(environment_id) - raise Gitlab::PrometheusError, "couldn't find cluster with Prometheus installed" unless cluster + raise Gitlab::PrometheusClient::Error, "couldn't find cluster with Prometheus installed" unless cluster rest_client = client_from_cluster(cluster) - raise Gitlab::PrometheusError, "couldn't create proxy Prometheus client" unless rest_client + raise Gitlab::PrometheusClient::Error, "couldn't create proxy Prometheus client" unless rest_client Gitlab::PrometheusClient.new(rest_client) end @@ -152,9 +152,11 @@ class PrometheusService < MonitoringService cluster.application_prometheus.proxy_client end - def rename_data_to_metrics(metrics) - metrics[:metrics] = metrics.delete :data - metrics + def rename_field(old_field, new_field) + -> (metrics) do + metrics[new_field] = metrics.delete(old_field) + metrics + end end def synchronize_service_state! diff --git a/app/models/tree.rb b/app/models/tree.rb index c89b8eca9be..4c1856b67a8 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -9,10 +9,9 @@ class Tree @repository = repository @sha = sha @path = path - @recursive = recursive git_repo = @repository.raw_repository - @entries = get_entries(git_repo, @sha, @path, recursive: @recursive) + @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive) end def readme @@ -58,21 +57,4 @@ class Tree def sorted_entries trees + blobs + submodules end - - private - - def get_entries(git_repo, sha, path, recursive: false) - current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path) - ordered_entries = [] - - current_path_entries.each do |entry| - ordered_entries << entry - - if recursive && entry.dir? - ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true)) - end - end - - ordered_entries - end end diff --git a/app/models/user.rb b/app/models/user.rb index f5eeba27572..8610ca27b7f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -327,8 +327,8 @@ class User < ActiveRecord::Base SQL where( - fuzzy_arel_match(:name, query) - .or(fuzzy_arel_match(:username, query)) + fuzzy_arel_match(:name, query, lower_exact_match: true) + .or(fuzzy_arel_match(:username, query, lower_exact_match: true)) .or(arel_table[:email].eq(query)) ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) end diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 8fa7b2753c7..603218aa6df 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -15,4 +15,7 @@ class BasePolicy < DeclarativePolicy::Base condition(:restricted_public_level, scope: :global) do Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) end + + # This is prevented in some cases in `gitlab-ee` + rule { default }.enable :read_cross_project end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index f0aa16d2ecf..3f6d7d04667 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -3,6 +3,19 @@ class IssuablePolicy < BasePolicy condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? } + # We aren't checking `:read_issue` or `:read_merge_request` in this case + # because it could be possible for a user to see an issuable-iid + # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be allowed + # to read the actual issue after a more expensive `:read_issue` check. + # + # `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee. + condition(:visible_to_user, score: 4) do + Project.where(id: @subject.project) + .public_or_visible_to_user(@user) + .with_feature_available_for_user(@subject, @user) + .any? + end + condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) } desc "User is the assignee or author" diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index bd2d417b2a8..ed499511999 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -13,7 +13,10 @@ class IssuePolicy < IssuablePolicy rule { confidential & ~can_read_confidential }.policy do prevent :read_issue + prevent :read_issue_iid prevent :update_issue prevent :admin_issue end + + rule { can?(:read_issue) | visible_to_user }.enable :read_issue_iid end diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb index bc3afc626fb..e003376d219 100644 --- a/app/policies/merge_request_policy.rb +++ b/app/policies/merge_request_policy.rb @@ -1,3 +1,3 @@ class MergeRequestPolicy < IssuablePolicy - # pass + rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 61a7bf02675..3b0550b4dd6 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -80,8 +80,9 @@ class ProjectPolicy < BasePolicy rule { reporter }.enable :reporter_access rule { developer }.enable :developer_access rule { master }.enable :master_access + rule { owner | admin }.enable :owner_access - rule { owner | admin }.policy do + rule { can?(:owner_access) }.policy do enable :guest_access enable :reporter_access enable :developer_access @@ -98,11 +99,6 @@ class ProjectPolicy < BasePolicy enable :remove_pages end - rule { owner | reporter }.policy do - enable :build_download_code - enable :build_read_container_image - end - rule { can?(:guest_access) }.policy do enable :read_project enable :read_board @@ -121,6 +117,11 @@ class ProjectPolicy < BasePolicy enable :read_cycle_analytics end + # These abilities are not allowed to admins that are not members of the project, + # that's why they are defined separatly. + rule { guest & can?(:download_code) }.enable :build_download_code + rule { guest & can?(:read_container_image) }.enable :build_read_container_image + rule { can?(:reporter_access) }.policy do enable :download_code enable :download_wiki_code @@ -140,12 +141,19 @@ class ProjectPolicy < BasePolicy enable :read_merge_request end + # We define `:public_user_access` separately because there are cases in gitlab-ee + # where we enable or prevent it based on other coditions. rule { (~anonymous & public_project) | internal_access }.policy do enable :public_user_access end rule { can?(:public_user_access) }.policy do + enable :public_access enable :guest_access + + enable :fork_project + enable :build_download_code + enable :build_read_container_image enable :request_access end @@ -196,14 +204,6 @@ class ProjectPolicy < BasePolicy enable :create_cluster end - rule { can?(:public_user_access) }.policy do - enable :public_access - - enable :fork_project - enable :build_download_code - enable :build_read_container_image - end - rule { archived }.policy do prevent :create_merge_request prevent :push_code diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb new file mode 100644 index 00000000000..484ac64580d --- /dev/null +++ b/app/presenters/project_presenter.rb @@ -0,0 +1,338 @@ +class ProjectPresenter < Gitlab::View::Presenter::Delegated + include ActionView::Helpers::NumberHelper + include ActionView::Helpers::UrlHelper + include GitlabRoutingHelper + include StorageHelper + include TreeHelper + include Gitlab::Utils::StrongMemoize + + presents :project + + def statistics_anchors(show_auto_devops_callout:) + [ + files_anchor_data, + commits_anchor_data, + branches_anchor_data, + tags_anchor_data, + readme_anchor_data, + changelog_anchor_data, + license_anchor_data, + contribution_guide_anchor_data, + gitlab_ci_anchor_data, + autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), + kubernetes_cluster_anchor_data + ].compact.select { |item| item.enabled } + end + + def statistics_buttons(show_auto_devops_callout:) + [ + changelog_anchor_data, + license_anchor_data, + contribution_guide_anchor_data, + autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), + kubernetes_cluster_anchor_data, + gitlab_ci_anchor_data, + koding_anchor_data + ].compact.reject { |item| item.enabled } + end + + def empty_repo_statistics_anchors + [ + autodevops_anchor_data, + kubernetes_cluster_anchor_data + ].compact.select { |item| item.enabled } + end + + def empty_repo_statistics_buttons + [ + new_file_anchor_data, + readme_anchor_data, + license_anchor_data, + autodevops_anchor_data, + kubernetes_cluster_anchor_data + ].compact.reject { |item| item.enabled } + end + + def default_view + return anonymous_project_view unless current_user + + user_view = current_user.project_view + + if can?(current_user, :download_code, project) + user_view + elsif user_view == "activity" + "activity" + elsif can?(current_user, :read_wiki, project) + "wiki" + elsif feature_available?(:issues, current_user) + "projects/issues/issues" + else + "customize_workflow" + end + end + + def readme_path + filename_path(:readme) + end + + def changelog_path + filename_path(:changelog) + end + + def license_path + filename_path(:license_blob) + end + + def ci_configuration_path + filename_path(:gitlab_ci_yml) + end + + def contribution_guide_path + if project && contribution_guide = repository.contribution_guide + project_blob_path( + project, + tree_join(project.default_branch, + contribution_guide.name) + ) + end + end + + def add_license_path + add_special_file_path(file_name: 'LICENSE') + end + + def add_changelog_path + add_special_file_path(file_name: 'CHANGELOG') + end + + def add_contribution_guide_path + add_special_file_path(file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') + end + + def add_ci_yml_path + add_special_file_path(file_name: '.gitlab-ci.yml') + end + + def add_readme_path + add_special_file_path(file_name: 'README.md') + end + + def add_koding_stack_path + project_new_blob_path( + project, + default_branch || 'master', + file_name: '.koding.yml', + commit_message: "Add Koding stack script", + content: <<-CONTENT.strip_heredoc + provider: + aws: + access_key: '${var.aws_access_key}' + secret_key: '${var.aws_secret_key}' + resource: + aws_instance: + #{project.path}-vm: + instance_type: t2.nano + user_data: |- + + # Created by GitLab UI for :> + + echo _KD_NOTIFY_@Installing Base packages...@ + + apt-get update -y + apt-get install git -y + + echo _KD_NOTIFY_@Cloning #{project.name}...@ + + export KODING_USER=${var.koding_user_username} + export REPO_URL=#{root_url}${var.koding_queryString_repo}.git + export BRANCH=${var.koding_queryString_branch} + + sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH + + echo _KD_NOTIFY_@#{project.name} cloned.@ + CONTENT + ) + end + + def license_short_name + license = repository.license + license&.nickname || license&.name || 'LICENSE' + end + + def can_current_user_push_code? + strong_memoize(:can_current_user_push_code) do + if empty_repo? + can?(current_user, :push_code, project) + else + can_current_user_push_to_branch?(default_branch) + end + end + end + + def can_current_user_push_to_branch?(branch) + return false unless repository.branch_exists?(branch) + + ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch) + end + + def files_anchor_data + OpenStruct.new(enabled: true, + label: _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, + link: project_tree_path(project)) + end + + def commits_anchor_data + OpenStruct.new(enabled: true, + label: n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, + link: project_commits_path(project, repository.root_ref)) + end + + def branches_anchor_data + OpenStruct.new(enabled: true, + label: n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, + link: project_branches_path(project)) + end + + def tags_anchor_data + OpenStruct.new(enabled: true, + label: n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, + link: project_tags_path(project)) + end + + def new_file_anchor_data + if current_user && can_current_user_push_code? + OpenStruct.new(enabled: false, + label: _('New file'), + link: project_new_blob_path(project, default_branch || 'master'), + class_modifier: 'new') + end + end + + def readme_anchor_data + if current_user && can_current_user_push_code? && repository.readme.blank? + OpenStruct.new(enabled: false, + label: _('Add Readme'), + link: add_readme_path) + elsif repository.readme.present? + OpenStruct.new(enabled: true, + label: _('Readme'), + link: default_view != 'readme' ? readme_path : '#readme') + end + end + + def changelog_anchor_data + if current_user && can_current_user_push_code? && repository.changelog.blank? + OpenStruct.new(enabled: false, + label: _('Add Changelog'), + link: add_changelog_path) + elsif repository.changelog.present? + OpenStruct.new(enabled: true, + label: _('Changelog'), + link: changelog_path) + end + end + + def license_anchor_data + if current_user && can_current_user_push_code? && repository.license_blob.blank? + OpenStruct.new(enabled: false, + label: _('Add License'), + link: add_license_path) + elsif repository.license_blob.present? + OpenStruct.new(enabled: true, + label: license_short_name, + link: license_path) + end + end + + def contribution_guide_anchor_data + if current_user && can_current_user_push_code? && repository.contribution_guide.blank? + OpenStruct.new(enabled: false, + label: _('Add Contribution guide'), + link: add_contribution_guide_path) + elsif repository.contribution_guide.present? + OpenStruct.new(enabled: true, + label: _('Contribution guide'), + link: contribution_guide_path) + end + end + + def autodevops_anchor_data(show_auto_devops_callout: false) + if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout + OpenStruct.new(enabled: auto_devops_enabled?, + label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), + link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + elsif auto_devops_enabled? + OpenStruct.new(enabled: true, + label: _('Auto DevOps enabled'), + link: nil) + end + end + + def kubernetes_cluster_anchor_data + if current_user && can?(current_user, :create_cluster, project) + cluster_link = clusters.count == 1 ? project_cluster_path(project, clusters.first) : project_clusters_path(project) + + if clusters.empty? + cluster_link = new_project_cluster_path(project) + end + + OpenStruct.new(enabled: !clusters.empty?, + label: clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), + link: cluster_link) + end + end + + def gitlab_ci_anchor_data + if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? + OpenStruct.new(enabled: false, + label: _('Set up CI/CD'), + link: add_ci_yml_path) + elsif repository.gitlab_ci_yml.present? + OpenStruct.new(enabled: true, + label: _('CI/CD configuration'), + link: ci_configuration_path) + end + end + + def koding_anchor_data + if current_user && can_current_user_push_code? && koding_enabled? && repository.koding_yml.blank? + OpenStruct.new(enabled: false, + label: _('Set up Koding'), + link: add_koding_stack_path) + end + end + + private + + def filename_path(filename) + if blob = repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend + project_blob_path( + project, + tree_join(default_branch, blob.name) + ) + end + end + + def anonymous_project_view + if !project.empty_repo? && can?(current_user, :download_code, project) + 'files' + else + 'activity' + end + end + + def add_special_file_path(file_name:, commit_message: nil, branch_name: nil) + commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name } + project_new_blob_path( + project, + project.default_branch || 'master', + file_name: file_name, + commit_message: commit_message, + branch_name: branch_name + ) + end + + def koding_enabled? + Gitlab::CurrentSettings.koding_enabled? + end +end diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb index aca4e4ca488..15ec0f89bb2 100644 --- a/app/serializers/group_child_entity.rb +++ b/app/serializers/group_child_entity.rb @@ -11,9 +11,7 @@ class GroupChildEntity < Grape::Entity end expose :can_edit do |instance| - return false unless request.respond_to?(:current_user) - - can?(request.current_user, "admin_#{type}", instance) + can_edit? end expose :edit_path do |instance| @@ -83,4 +81,17 @@ class GroupChildEntity < Grape::Entity def markdown_description markdown_field(object, :description) end + + def can_edit? + return false unless request.respond_to?(:current_user) + + if project? + # Avoid checking rights for each project, as it might be expensive if the + # user cannot read cross project. + can?(request.current_user, :read_cross_project) && + can?(request.current_user, :admin_project, object) + else + can?(request.current_user, :admin_group, object) + end + end end diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb index 280a2c3afa4..ffde824972c 100644 --- a/app/services/ci/create_trace_artifact_service.rb +++ b/app/services/ci/create_trace_artifact_service.rb @@ -4,13 +4,33 @@ module Ci return if job.job_artifacts_trace job.trace.read do |stream| - if stream.file? - job.create_job_artifacts_trace!( - project: job.project, - file_type: :trace, - file: stream) + break unless stream.file? + + clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path| + create_job_trace!(job, clone_path) + FileUtils.rm(stream.path) end end end + + private + + def create_job_trace!(job, path) + File.open(path) do |stream| + job.create_job_artifacts_trace!( + project: job.project, + file_type: :trace, + file: stream) + end + end + + def clone_file!(src_path, temp_dir) + FileUtils.mkdir_p(temp_dir) + Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path| + temp_path = File.join(dir_path, "job.log") + FileUtils.copy(src_path, temp_path) + yield(temp_path) + end + end end end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index cea56f4e849..15ab2d54404 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -30,10 +30,10 @@ module Clusters ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), username: gke_cluster.master_auth.username, password: gke_cluster.master_auth.password, - token: request_kuberenetes_token) + token: request_kubernetes_token) end - def request_kuberenetes_token + def request_kubernetes_token Ci::FetchKubernetesTokenService.new( 'https://' + gke_cluster.endpoint, Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e7463e6e25c..e87fd49d193 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -77,8 +77,12 @@ class IssuableBaseService < BaseService return unless labels params[:label_ids] = labels.split(",").map do |label_name| - service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) - label = service.execute + label = Labels::FindOrCreateService.new( + current_user, + parent, + title: label_name.strip, + available_labels: available_labels + ).execute label.try(:id) end.compact @@ -102,7 +106,7 @@ class IssuableBaseService < BaseService end def available_labels - LabelsFinder.new(current_user, project_id: @project.id).execute + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end def merge_quick_actions_into_params!(issuable) @@ -247,7 +251,7 @@ class IssuableBaseService < BaseService when 'add' todo_service.mark_todo(issuable, current_user) when 'done' - todo = TodosFinder.new(current_user).execute.find_by(target: issuable) + todo = TodosFinder.new(current_user).find_by(target: issuable) todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo end end @@ -303,4 +307,8 @@ class IssuableBaseService < BaseService def update_project_counter_caches?(issuable) issuable.state_changed? end + + def parent + project + end end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index 940c8b333d3..079f611b3f3 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -1,8 +1,9 @@ module Labels class FindOrCreateService - def initialize(current_user, project, params = {}) + def initialize(current_user, parent, params = {}) @current_user = current_user - @project = project + @parent = parent + @available_labels = params.delete(:available_labels) @params = params.dup.with_indifferent_access end @@ -13,12 +14,13 @@ module Labels private - attr_reader :current_user, :project, :params, :skip_authorization + attr_reader :current_user, :parent, :params, :skip_authorization def available_labels @available_labels ||= LabelsFinder.new( current_user, - project_id: project.id + "#{parent_type}_id".to_sym => parent.id, + only_group_labels: parent_is_group? ).execute(skip_authorization: skip_authorization) end @@ -27,8 +29,8 @@ module Labels def find_or_create_label new_label = available_labels.find_by(title: title) - if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) - new_label = Labels::CreateService.new(params).execute(project: project) + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent)) + new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent) end new_label @@ -37,5 +39,13 @@ module Labels def title params[:title] || params[:name] end + + def parent_type + parent.model_name.param_key + end + + def parent_is_group? + parent_type == "group" + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 56e941d90ff..e07ecda27b5 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -339,6 +339,30 @@ class NotificationService end end + def pages_domain_verification_succeeded(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later + end + end + + def pages_domain_verification_failed(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_verification_failed_email(domain, user).deliver_later + end + end + + def pages_domain_enabled(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_enabled_email(domain, user).deliver_later + end + end + + def pages_domain_disabled(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_disabled_email(domain, user).deliver_later + end + end + protected def new_resource_email(target, method) @@ -433,6 +457,14 @@ class NotificationService private + def recipients_for_pages_domain(domain) + project = domain.project + + return [] unless project + + notifiable_users(project.team.masters, :watch, target: project) + end + def notifiable?(*args) NotificationRecipientService.notifiable?(*args) end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 1ae2c40872a..e61ecb696d0 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -50,16 +50,7 @@ module Projects return [] unless noteable&.is_a?(Issuable) - opts = { - project: project, - issuable: noteable, - current_user: current_user - } - QuickActions::InterpretService.command_definitions.map do |definition| - next unless definition.available?(opts) - - definition.to_h(opts) - end.compact + QuickActions::InterpretService.new(project, current_user).available_commands(noteable) end end end diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index cacb74b1205..52ff64cc938 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -23,7 +23,7 @@ module Projects end def pages_domains_config - project.pages_domains.map do |domain| + enabled_pages_domains.map do |domain| { domain: domain.domain, certificate: domain.certificate, @@ -32,6 +32,14 @@ module Projects end end + def enabled_pages_domains + if Gitlab::CurrentSettings.pages_domain_verification_enabled? + project.pages_domains.enabled + else + project.pages_domains + end + end + def reload_daemon # GitLab Pages daemon constantly watches for modification time of `pages.path` # It reloads configuration when `pages.path` is modified diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 669c1ba0a22..1e9bd84e749 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -7,6 +7,18 @@ module QuickActions SHRUG = '¯\\_(ツ)_/¯'.freeze TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze + # Takes an issuable and returns an array of all the available commands + # represented with .to_h + def available_commands(issuable) + @issuable = issuable + + self.class.command_definitions.map do |definition| + next unless definition.available?(self) + + definition.to_h(self) + end.compact + end + # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. def execute(content, issuable) @@ -15,8 +27,8 @@ module QuickActions @issuable = issuable @updates = {} - content, commands = extractor.extract_commands(content, context) - extract_updates(commands, context) + content, commands = extractor.extract_commands(content) + extract_updates(commands) [content, @updates] end @@ -28,8 +40,8 @@ module QuickActions @issuable = issuable - content, commands = extractor.extract_commands(content, context) - commands = explain_commands(commands, context) + content, commands = extractor.extract_commands(content) + commands = explain_commands(commands) [content, commands] end @@ -157,11 +169,11 @@ module QuickActions params '%"milestone"' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) && - project.milestones.active.any? + find_milestones(project, state: 'active').any? end parse_params do |milestone_param| extract_references(milestone_param, :milestone).first || - project.milestones.find_by(title: milestone_param.strip) + find_milestones(project, title: milestone_param.strip).first end command :milestone do |milestone| @updates[:milestone_id] = milestone.id if milestone @@ -544,6 +556,10 @@ module QuickActions users end + def find_milestones(project, params = {}) + MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: [project.group&.id])).execute + end + def find_labels(labels_param) extract_references(labels_param, :label) | LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute @@ -557,21 +573,21 @@ module QuickActions find_labels(labels_param).map(&:id) end - def explain_commands(commands, opts) + def explain_commands(commands) commands.map do |name, arg| definition = self.class.definition_by_name(name) next unless definition - definition.explain(self, opts, arg) + definition.explain(self, arg) end.compact end - def extract_updates(commands, opts) + def extract_updates(commands) commands.each do |name, arg| definition = self.class.definition_by_name(name) next unless definition - definition.execute(self, opts, arg) + definition.execute(self, arg) end end @@ -581,14 +597,5 @@ module QuickActions ext.references(type) end - - def context - { - issuable: issuable, - current_user: current_user, - project: project, - params: params - } - end end end diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb new file mode 100644 index 00000000000..86166047302 --- /dev/null +++ b/app/services/verify_pages_domain_service.rb @@ -0,0 +1,107 @@ +require 'resolv' + +class VerifyPagesDomainService < BaseService + # The maximum number of seconds to be spent on each DNS lookup + RESOLVER_TIMEOUT_SECONDS = 15 + + # How long verification lasts for + VERIFICATION_PERIOD = 7.days + + attr_reader :domain + + def initialize(domain) + @domain = domain + end + + def execute + return error("No verification code set for #{domain.domain}") unless domain.verification_code.present? + + if !verification_enabled? || dns_record_present? + verify_domain! + elsif expired? + disable_domain! + else + unverify_domain! + end + end + + private + + def verify_domain! + was_disabled = !domain.enabled? + was_unverified = domain.unverified? + + # Prevent any pre-existing grace period from being truncated + reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max + + domain.update!(verified_at: Time.now, enabled_until: reverify) + + if was_disabled + notify(:enabled) + elsif was_unverified + notify(:verification_succeeded) + end + + success + end + + def unverify_domain! + if domain.verified? + domain.update!(verified_at: nil) + notify(:verification_failed) + end + + error("Couldn't verify #{domain.domain}") + end + + def disable_domain! + domain.update!(verified_at: nil, enabled_until: nil) + + notify(:disabled) + + error("Couldn't verify #{domain.domain}. It is now disabled.") + end + + # A domain is only expired until `disable!` has been called + def expired? + domain.enabled_until && domain.enabled_until < Time.now + end + + def dns_record_present? + Resolv::DNS.open do |resolver| + resolver.timeouts = RESOLVER_TIMEOUT_SECONDS + + check(domain.domain, resolver) || check(domain.verification_domain, resolver) + end + end + + def check(domain_name, resolver) + records = parse(txt_records(domain_name, resolver)) + + records.any? do |record| + record == domain.keyed_verification_code || record == domain.verification_code + end + rescue => err + log_error("Failed to check TXT records on #{domain_name} for #{domain.domain}: #{err}") + false + end + + def txt_records(domain_name, resolver) + resolver.getresources(domain_name, Resolv::DNS::Resource::IN::TXT) + end + + def parse(records) + records.flat_map(&:strings).flat_map(&:split) + end + + def verification_enabled? + Gitlab::CurrentSettings.pages_domain_verification_enabled? + end + + def notify(type) + return unless verification_enabled? + + Gitlab::AppLogger.info("Pages domain '#{domain.domain}' changed state to '#{type}'") + notification_service.public_send("pages_domain_#{type}", domain) # rubocop:disable GitlabSecurity/PublicSend + end +end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index a9e5c028b03..010100f2da1 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -67,6 +67,10 @@ class GitlabUploader < CarrierWave::Uploader::Base super || file&.filename end + def model_valid? + !!model + end + private # Designed to be overridden by child uploaders that have a dynamic path diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index e7d9ecd3222..f2ad0badd53 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -14,6 +14,12 @@ class PersonalFileUploader < FileUploader File.join(model.class.to_s.underscore, model.id.to_s) end + # model_path_segment does not require a model to be passed, so we can always + # generate a path, even when there's no model. + def model_valid? + true + end + # Revert-Override def store_dir File.join(base_dir, dynamic_segment) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 938185b6eba..20527d31870 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -237,6 +237,17 @@ .col-sm-10 = f.number_field :max_pages_size, class: 'form-control' .help-block 0 for unlimited + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :pages_domain_verification_enabled do + = f.check_box :pages_domain_verification_enabled + Require users to prove ownership of custom domains + .help-block + Domain verification is an essential security measure for public GitLab + sites. Users are required to demonstrate they control a domain before + it is enabled + = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') %fieldset %legend Continuous Integration and Deployment @@ -647,11 +658,8 @@ = f.label :version_check_enabled do = f.check_box :version_check_enabled Version check enabled - = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") .help-block - Let GitLab inform you when an update is available. When - enabled, GitLab Inc. will collect info about your hostname - and version. + Let GitLab inform you when an update is available. .form-group .col-sm-offset-2.col-sm-10 - can_be_configured = @application_setting.usage_ping_can_be_configured? diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 1e52646b1cc..abec3607cab 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -35,9 +35,8 @@ method: :put, class: 'btn btn-default', data: { confirm: _("Are you sure you want to reset registration token?") } - = render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token, - type: 'shared' } + = render partial: 'ci/runner/how_to_setup_shared_runner', + locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } .append-bottom-20.clearfix .pull-left diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index 8db7727b80c..37fb8fbab26 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -1,16 +1,16 @@ - link = link_to _("GitLab Runner section"), 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank' -.bs-callout.help-callout - %h4= _("How to setup a #{type} Runner for a new project") +.append-bottom-10 + %h4= _("Setup a #{type} Runner manually") - %ol - %li - = _("Install a Runner compatible with GitLab CI") - = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe - %li - = _("Specify the following URL during the Runner setup:") - %code#coordinator_address= root_url(only_path: false) - %li - = _("Use the following registration token during setup:") - %code#registration_token= registration_token - %li - = _("Start the Runner!") +%ol + %li + = _("Install a Runner compatible with GitLab CI") + = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe + %li + = _("Specify the following URL during the Runner setup:") + %code#coordinator_address= root_url(only_path: false) + %li + = _("Use the following registration token during setup:") + %code#registration_token= registration_token + %li + = _("Start the Runner!") diff --git a/app/views/ci/runner/_how_to_setup_shared_runner.html.haml b/app/views/ci/runner/_how_to_setup_shared_runner.html.haml new file mode 100644 index 00000000000..2a190cb9250 --- /dev/null +++ b/app/views/ci/runner/_how_to_setup_shared_runner.html.haml @@ -0,0 +1,3 @@ +.bs-callout.help-callout + = render partial: 'ci/runner/how_to_setup_runner', + locals: { registration_token: registration_token, type: 'shared' } diff --git a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml b/app/views/ci/runner/_how_to_setup_specific_runner.html.haml new file mode 100644 index 00000000000..e765a353fe4 --- /dev/null +++ b/app/views/ci/runner/_how_to_setup_specific_runner.html.haml @@ -0,0 +1,26 @@ +.bs-callout.help-callout + .append-bottom-10 + %h4= _('Setup a specific Runner automatically') + + %p + - link_to_help_page = link_to(_('Learn more about Kubernetes'), + help_page_path('user/project/clusters/index'), + target: '_blank', + rel: 'noopener noreferrer') + + = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page } + + %ol + %li + = _('Click the button below to begin the install process by navigating to the Kubernetes page') + %li + = _('Select an existing Kubernetes cluster or create a new one') + %li + = _('From the Kubernetes cluster details view, install Runner from the applications list') + + = link_to _('Install Runner on Kubernetes'), + project_clusters_path(@project), + class: 'btn btn-info' + %hr + = render partial: 'ci/runner/how_to_setup_runner', + locals: { registration_token: registration_token, type: 'specific' } diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index a97cbd4d4b3..bf540439c79 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,3 +1,5 @@ +- message = local_assigns.fetch(:message) + - content_for(:title, 'Access Denied') %img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } %h1 @@ -5,5 +7,9 @@ .container %h3 Access Denied %hr - %p You are not allowed to access this page. - %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} + - if message + %p + = message + - else + %p You are not allowed to access this page. + %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 1c4d67a8d2c..ce09b44fbb2 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -1,7 +1,5 @@ - page_title "UI Development Kit", "Help" - lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare." -- content_for :page_specific_javascripts do - = webpack_bundle_tag('ui_development_kit') .gitlab-ui-dev-kit %h1 GitLab UI development kit diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 1d00ae928f6..e6238c0dddb 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -20,29 +20,34 @@ %ul.nav.navbar-nav - if current_user = render 'layouts/header/new_dropdown' - %li.hidden-sm.hidden-xs - = render 'layouts/search' unless current_controller?(:search) - %li.visible-sm-inline-block.visible-xs-inline-block - = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = sprite_icon('search', size: 16) - - if current_user + - if header_link?(:search) + %li.hidden-sm.hidden-xs + = render 'layouts/search' unless current_controller?(:search) + %li.visible-sm-inline-block.visible-xs-inline-block + = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = sprite_icon('search', size: 16) + + - if header_link?(:issues) = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('issues', size: 16) - issues_count = assigned_issuables_count(:issues) %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) + - if header_link?(:merge_requests) = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('git-merge', size: 16) - merge_requests_count = assigned_issuables_count(:merge_requests) %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } = number_with_delimiter(merge_requests_count) + - if header_link?(:todos) = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('todo-done', size: 16) %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) + - if header_link?(:user_dropdown) %li.header-user.dropdown = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" @@ -64,11 +69,11 @@ %li.divider %li = link_to "Sign out", destroy_user_session_path, class: "sign-out-link" - - if session[:impersonator_id] - %li.impersonation - = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do - = icon('user-secret') - - else + - if header_link?(:admin_impersonation) + %li.impersonation + = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = icon('user-secret') + - if header_link?(:sign_in) %li %div = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 74532eba298..f773bd0832d 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,53 +1,64 @@ %ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do - %a{ href: "#", data: { toggle: "dropdown" } } - Projects - = sprite_icon('angle-down', css_class: 'caret-down') - .dropdown-menu.projects-dropdown-menu - = render "layouts/nav/projects_dropdown/show" + - if dashboard_nav_link?(:projects) + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do + %a{ href: "#", data: { toggle: "dropdown" } } + Projects + = sprite_icon('angle-down', css_class: 'caret-down') + .dropdown-menu.projects-dropdown-menu + = render "layouts/nav/projects_dropdown/show" - = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do - = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do - Groups + - if dashboard_nav_link?(:groups) + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do + = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do + Groups - = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - Activity + - if dashboard_nav_link?(:activity) + = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + Activity - = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do - = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do - Milestones + - if dashboard_nav_link?(:milestones) + = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do + = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do + Milestones - = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do - = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do - Snippets + - if dashboard_nav_link?(:snippets) + = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do + = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do + Snippets - %li.header-more.dropdown.hidden-lg - %a{ href: "#", data: { toggle: "dropdown" } } - More - = sprite_icon('angle-down', css_class: 'caret-down') - .dropdown-menu - %ul - = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do - = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do - Groups + - if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets]) + %li.header-more.dropdown.hidden-lg + %a{ href: "#", data: { toggle: "dropdown" } } + More + = sprite_icon('angle-down', css_class: 'caret-down') + .dropdown-menu + %ul + - if dashboard_nav_link?(:groups) + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do + = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do + Groups - = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, title: 'Activity' do - Activity + - if dashboard_nav_link?(:activity) + = nav_link(path: 'dashboard#activity') do + = link_to activity_dashboard_path, title: 'Activity' do + Activity - = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do - Milestones + - if dashboard_nav_link?(:milestones) + = nav_link(controller: 'dashboard/milestones') do + = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do + Milestones - = nav_link(controller: 'dashboard/snippets') do - = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do - Snippets + - if dashboard_nav_link?(:snippets) + = nav_link(controller: 'dashboard/snippets') do + = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do + Snippets -# Shortcut to Dashboard > Projects - %li.hidden - = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - Projects + - if dashboard_nav_link?(:projects) + %li.hidden + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + Projects - if current_controller?('ide') %li.line-separator.hidden-xs diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index cd1c39f3226..50bde9d1754 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -1,12 +1,15 @@ %ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do - = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - Projects - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do - = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do - Groups - = nav_link(controller: :snippets) do - = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do - Snippets + - if explore_nav_link?(:projects) + = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do + = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + Projects + - if explore_nav_link?(:groups) + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do + = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do + Groups + - if explore_nav_link?(:snippets) + = nav_link(controller: :snippets) do + = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do + Snippets %li = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 09a43a2cac5..47ae79b7a69 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,6 +1,8 @@ - issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count - merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count +- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] + .nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll .context-header @@ -10,84 +12,93 @@ .sidebar-context-title = @group.name %ul.sidebar-top-level-items - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group) do - .nav-icon-container - = sprite_icon('project') - %span.nav-item-name - Overview + - if group_sidebar_link?(:overview) + = 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') + %span.nav-item-name + Overview - %ul.sidebar-sub-level-items - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do - = link_to group_path(@group) do - %strong.fly-out-top-item-name - #{ _('Overview') } - %li.divider.fly-out-top-item - = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group details' do - %span - Details + %ul.sidebar-sub-level-items + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do + = link_to group_path(@group) do + %strong.fly-out-top-item-name + #{ _('Overview') } + %li.divider.fly-out-top-item + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group details' do + %span + Details - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - %span - Activity + - if group_sidebar_link?(:activity) + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + %span + Activity - = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = link_to issues_group_path(@group) do - .nav-icon-container - = sprite_icon('issues') - %span.nav-item-name - Issues - %span.badge.count= number_with_delimiter(issues_count) + - if group_sidebar_link?(:issues) + = nav_link(path: issues_sub_menu_items) do + = link_to issues_group_path(@group) do + .nav-icon-container + = sprite_icon('issues') + %span.nav-item-name + Issues + %span.badge.count= number_with_delimiter(issues_count) - %ul.sidebar-sub-level-items - = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do - = link_to issues_group_path(@group) do - %strong.fly-out-top-item-name - #{ _('Issues') } - %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count) - %li.divider.fly-out-top-item - = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do - = link_to issues_group_path(@group), title: 'List' do - %span - List + %ul.sidebar-sub-level-items + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do + = link_to issues_group_path(@group) do + %strong.fly-out-top-item-name + #{ _('Issues') } + %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count) + %li.divider.fly-out-top-item + = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: 'List' do + %span + List + + - if group_sidebar_link?(:labels) + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: 'Labels' do + %span + Labels - = nav_link(path: 'labels#index') do - = link_to group_labels_path(@group), title: 'Labels' do - %span - Labels + - if group_sidebar_link?(:milestones) + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: 'Milestones' do + %span + Milestones + + - if group_sidebar_link?(:merge_requests) + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group) do + .nav-icon-container + = sprite_icon('git-merge') + %span.nav-item-name + Merge Requests + %span.badge.count= number_with_delimiter(merge_requests_count) + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do + = link_to merge_requests_group_path(@group) do + %strong.fly-out-top-item-name + #{ _('Merge Requests') } + %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count) - = nav_link(path: 'milestones#index') do - = link_to group_milestones_path(@group), title: 'Milestones' do - %span - Milestones + - if group_sidebar_link?(:group_members) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group) do + .nav-icon-container + = sprite_icon('users') + %span.nav-item-name + Members + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do + = link_to group_group_members_path(@group) do + %strong.fly-out-top-item-name + #{ _('Members') } - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group) do - .nav-icon-container - = sprite_icon('git-merge') - %span.nav-item-name - Merge Requests - %span.badge.count= number_with_delimiter(merge_requests_count) - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do - = link_to merge_requests_group_path(@group) do - %strong.fly-out-top-item-name - #{ _('Merge Requests') } - %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count) - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group) do - .nav-icon-container - = sprite_icon('users') - %span.nav-item-name - Members - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do - = link_to group_group_members_path(@group) do - %strong.fly-out-top-item-name - #{ _('Members') } - - if current_user && can?(current_user, :admin_group, @group) + - if group_sidebar_link?(:settings) = nav_link(path: group_nav_link_paths) do = link_to edit_group_path(@group) do .nav-icon-container diff --git a/app/views/notify/pages_domain_disabled_email.html.haml b/app/views/notify/pages_domain_disabled_email.html.haml new file mode 100644 index 00000000000..34ce4238a12 --- /dev/null +++ b/app/views/notify/pages_domain_disabled_email.html.haml @@ -0,0 +1,15 @@ +%p + Following a verification check, your GitLab Pages custom domain has been + %strong disabled. + This means that your content is no longer visible at #{link_to @domain.url, @domain.url} +%p + Project: #{link_to @project.human_name, project_url(@project)} +%p + Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)} +%p + If this domain has been disabled in error, please follow + = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + to verify and re-enable your domain. +%p + If you no longer wish to use this domain with GitLab Pages, please remove it + from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/pages_domain_disabled_email.text.haml b/app/views/notify/pages_domain_disabled_email.text.haml new file mode 100644 index 00000000000..4e81b054b1f --- /dev/null +++ b/app/views/notify/pages_domain_disabled_email.text.haml @@ -0,0 +1,13 @@ +Following a verification check, your GitLab Pages custom domain has been +**disabled**. This means that your content is no longer visible at #{@domain.url} + +Project: #{@project.human_name} (#{project_url(@project)}) +Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)}) + +If this domain has been disabled in error, please follow these instructions +to verify and re-enable your domain: + += help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + +If you no longer wish to use this domain with GitLab Pages, please remove it +from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/pages_domain_enabled_email.html.haml b/app/views/notify/pages_domain_enabled_email.html.haml new file mode 100644 index 00000000000..db09e503f65 --- /dev/null +++ b/app/views/notify/pages_domain_enabled_email.html.haml @@ -0,0 +1,11 @@ +%p + Following a verification check, your GitLab Pages custom domain has been + enabled. You should now be able to view your content at #{link_to @domain.url, @domain.url} +%p + Project: #{link_to @project.human_name, project_url(@project)} +%p + Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)} +%p + Please visit + = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + for more information about custom domain verification. diff --git a/app/views/notify/pages_domain_enabled_email.text.haml b/app/views/notify/pages_domain_enabled_email.text.haml new file mode 100644 index 00000000000..1ed1dbb8315 --- /dev/null +++ b/app/views/notify/pages_domain_enabled_email.text.haml @@ -0,0 +1,9 @@ +Following a verification check, your GitLab Pages custom domain has been +enabled. You should now be able to view your content at #{@domain.url} + +Project: #{@project.human_name} (#{project_url(@project)}) +Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)}) + +Please visit += help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') +for more information about custom domain verification. diff --git a/app/views/notify/pages_domain_verification_failed_email.html.haml b/app/views/notify/pages_domain_verification_failed_email.html.haml new file mode 100644 index 00000000000..0bb0eb09fd5 --- /dev/null +++ b/app/views/notify/pages_domain_verification_failed_email.html.haml @@ -0,0 +1,17 @@ +%p + Verification has failed for one of your GitLab Pages custom domains! +%p + Project: #{link_to @project.human_name, project_url(@project)} +%p + Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)} +%p + Unless you take action, it will be disabled on + %strong= @domain.enabled_until.strftime('%F %T.') + Until then, you can view your content at #{link_to @domain.url, @domain.url} +%p + Please visit + = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + for more information about custom domain verification. +%p + If you no longer wish to use this domain with GitLab Pages, please remove it + from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/pages_domain_verification_failed_email.text.haml b/app/views/notify/pages_domain_verification_failed_email.text.haml new file mode 100644 index 00000000000..c14e0e0c24d --- /dev/null +++ b/app/views/notify/pages_domain_verification_failed_email.text.haml @@ -0,0 +1,14 @@ +Verification has failed for one of your GitLab Pages custom domains! + +Project: #{@project.human_name} (#{project_url(@project)}) +Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)}) + +Unless you take action, it will be disabled on *#{@domain.enabled_until.strftime('%F %T')}*. +Until then, you can view your content at #{@domain.url} + +Please visit += help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') +for more information about custom domain verification. + +If you no longer wish to use this domain with GitLab Pages, please remove it +from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/pages_domain_verification_succeeded_email.html.haml b/app/views/notify/pages_domain_verification_succeeded_email.html.haml new file mode 100644 index 00000000000..2ead3187b10 --- /dev/null +++ b/app/views/notify/pages_domain_verification_succeeded_email.html.haml @@ -0,0 +1,13 @@ +%p + One of your GitLab Pages custom domains has been successfully verified! +%p + Project: #{link_to @project.human_name, project_url(@project)} +%p + Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)} +%p + This is a notification. No action is required on your part. You can view your + content at #{link_to @domain.url, @domain.url} +%p + Please visit + = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + for more information about custom domain verification. diff --git a/app/views/notify/pages_domain_verification_succeeded_email.text.haml b/app/views/notify/pages_domain_verification_succeeded_email.text.haml new file mode 100644 index 00000000000..e7cdbdee420 --- /dev/null +++ b/app/views/notify/pages_domain_verification_succeeded_email.text.haml @@ -0,0 +1,10 @@ +One of your GitLab Pages custom domains has been successfully verified! + +Project: #{@project.human_name} (#{project_url(@project)}) +Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)}) + +No action is required on your part. You can view your content at #{@domain.url} + +Please visit += help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') +for more information about custom domain verification. diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml index 8129c72feb2..f455522d17c 100644 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml @@ -3,7 +3,7 @@ .radio = label_tag :project_merge_method_ff do - = form.radio_button :merge_method, :ff, class: "js-merge-method-radio" + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff" %strong Fast-forward merge %br %span.descr diff --git a/app/views/projects/_new_project_push_tip.html.haml b/app/views/projects/_new_project_push_tip.html.haml new file mode 100644 index 00000000000..9bc69211d12 --- /dev/null +++ b/app/views/projects/_new_project_push_tip.html.haml @@ -0,0 +1,11 @@ +.push-to-create-popover + %p + = label_tag(:push_to_create_tip, _("Private projects can be created in your personal namespace with:"), class: "weight-normal") + + %p.input-group.project-tip-command + %span.input-group-btn + = text_field_tag :push_to_create_tip, push_to_create_project_command, class: "js-select-on-focus form-control monospace", readonly: true, aria: { label: _("Push project from command line") } + %span.input-group-btn + = clipboard_button(text: push_to_create_project_command, title: _("Copy command to clipboard"), placement: "right") + %p + = link_to("What does this command do?", help_page_path("gitlab-basics/create-project", anchor: "push-to-create-a-new-project"), target: "_blank") diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index aebdfbc8218..705338c083e 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -20,4 +20,4 @@ distributed with computer software, forming part of its documentation. GitLab will render it here instead of this message. %p - = link_to "Add Readme", add_special_file_path(@project, file_name: 'README.md'), class: 'btn btn-new' + = link_to "Add Readme", @project.add_readme_path, class: 'btn btn-new' diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml new file mode 100644 index 00000000000..a115b65938b --- /dev/null +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -0,0 +1,8 @@ +- anchors = local_assigns.fetch(:anchors, []) + +- return unless anchors.any? +%ul.nav + - anchors.each do |anchor| + %li + = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'stat-link' : "btn btn-#{anchor.class_modifier || 'missing'}" do + %span.stat-text= anchor.label diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index 2a77dedd9a2..1b150ec3e5c 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -11,8 +11,8 @@ = view_on_environment_button(@commit.sha, @path, @environment) if @environment .btn-group{ role: "group" }< - = edit_blob_link - = ide_blob_link + = edit_blob_button + = ide_edit_button - if current_user = replace_blob_link = delete_blob_link diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml index de2d61d4aa3..e665ca61da8 100644 --- a/app/views/projects/buttons/_koding.html.haml +++ b/app/views/projects/buttons/_koding.html.haml @@ -1,3 +1,3 @@ -- if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch) +- if koding_enabled? && current_user && @repository.koding_yml && @project.can_current_user_push_code? = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do _('Run in IDE (Koding)') diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index 600d679b60c..112dde66ff7 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -4,7 +4,7 @@ .col-xs-12 .text-content %h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation') - - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') + - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} .text-center diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 0b01e38d23d..47bfcb21cf4 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -17,7 +17,7 @@ \ - if editable_diff?(diff_file) - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} - = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, + = edit_blob_button(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) - if image_diff && image_replaced diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0931ceb1512..b947b91322d 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -85,7 +85,7 @@ .settings-content = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = render 'merge_request_settings', form: f - = f.submit 'Save changes', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes" = render 'export', project: @project diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index ab225796b12..8a36fada389 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -5,38 +5,41 @@ = render "home_panel" -.row-content-block.second-block.center - %h4 - The repository for this project is empty +.project-empty-note-panel + %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] } + .prepend-top-20 + %h4 + = _('The repository for this project is empty') + + - if @project.can_current_user_push_code? + %p + - link_to_cli = link_to _('command line instructions'), '#repo-command-line-instructions' + = _('If you already have files you can push them using the %{link_to_cli} below.').html_safe % { link_to_cli: link_to_cli } + %p + %em + - link_to_protected_branches = link_to _('Learn more about protected branches'), help_page_path('user/project/protected_branches') + = _('Note that the master branch is automatically protected. %{link_to_protected_branches}').html_safe % { link_to_protected_branches: link_to_protected_branches } - - if can?(current_user, :push_code, @project) - %p - If you already have files you can push them using command line instructions below. - %p - Otherwise you can start with adding a - = succeed ',' do - = link_to "README", add_special_file_path(@project, file_name: 'README.md') - a - = succeed ',' do - = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE') - or a - = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore') - to this project. - %p - You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected. + %hr + %p + - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) + - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project)) + = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster } - - if show_auto_devops_callout?(@project) + %hr %p - - link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) - = s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link } - %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') - %p= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master'), class: 'btn btn-new' + = _('Otherwise it is recommended you start with one of the options below.') + .prepend-top-20 + +%nav.project-stats{ class: container_class } + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons - if can?(current_user, :push_code, @project) - %div{ class: container_class } + %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] } .prepend-top-20 .empty_wrapper - %h3.page-title-empty + %h3#repo-command-line-instructions.page-title-empty Command line instructions .git-empty %fieldset diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 91f68d8c419..d63443c9da5 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -55,7 +55,7 @@ .issue-details.issuable-details .detail-page-description.content-block - %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue) + %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json #js-issuable-app %h2.title= markdown_field(@issue, :title) - if @issue.description.present? @@ -82,6 +82,3 @@ = render 'projects/issues/discussion' = render 'shared/issuable/sidebar', issuable: @issue - -= webpack_bundle_tag('common_vue') -= webpack_bundle_tag('issue_show') diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 61ae0ebbce6..679ba23a4db 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -4,6 +4,7 @@ - page_title 'New Project' - header_title "Projects", dashboard_projects_path - visibility_level = params.dig(:project, :visibility_level) || default_project_visibility +- active_tab = local_assigns.fetch(:active_tab, 'blank') .project-edit-container .project-edit-errors @@ -18,34 +19,41 @@ All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings. .md = brand_new_project_guidelines + %p + %strong= _("Tip:") + = _("You can also create a project from the command line.") + %a.push-new-project-tip{ data: { title: _("Push to create a project") }, href: help_page_path('gitlab-basics/create-project', anchor: 'push-to-create-a-new-project'), target: "_blank", rel: "noopener noreferrer" } + = _("Show command") + %template.push-new-project-tip-template= render partial: "new_project_push_tip" + .col-lg-9.js-toggle-container %ul.nav-links.gitlab-tabs{ role: 'tablist' } - %li.active{ role: 'presentation' } + %li{ class: ('active' if active_tab == 'blank'), role: 'presentation' } %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Blank project %span.visible-xs Blank - %li{ role: 'presentation' } + %li{ class: ('active' if active_tab == 'template'), role: 'presentation' } %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Create from template %span.visible-xs Template - %li{ role: 'presentation' } + %li{ class: ('active' if active_tab == 'import'), role: 'presentation' } %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Import project %span.visible-xs Import .tab-content.gitlab-tab-content - .tab-pane.active{ id: 'blank-project-pane', role: 'tabpanel' } + .tab-pane{ id: 'blank-project-pane', class: ('active' if active_tab == 'blank'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| = render 'new_project_fields', f: f, project_name_id: "blank-project-name" - .tab-pane.no-padding{ id: 'create-from-template-pane', role: 'tabpanel' } + .tab-pane.no-padding{ id: 'create-from-template-pane', class: ('active' if active_tab == 'template'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| .project-template .form-group %div = render 'project_templates', f: f - .tab-pane.import-project-pane{ id: 'import-project-pane', role: 'tabpanel' } + .tab-pane.import-project-pane{ id: 'import-project-pane', class: ('active' if active_tab == 'import'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| - if import_sources_enabled? .project-import.row @@ -92,7 +100,7 @@ %button.btn.js-toggle-button.import_git{ type: "button" } = icon('git', text: 'Repo by URL') .col-lg-12 - .js-toggle-content.hide.toggle-import-form + .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } %hr = render "shared/import_form", f: f = render 'new_project_fields', f: f, project_name_id: "import-url-name" diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index a85cda407af..75df92b05a7 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -3,15 +3,26 @@ .panel-heading Domains (#{@domains.count}) %ul.well-list + - verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? - @domains.each do |domain| %li .pull-right = link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped" = link_to 'Remove', project_pages_domain_path(@project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" .clearfix - %span= link_to domain.domain, domain.url + - if verification_enabled + - tooltip, status = domain.unverified? ? ['Unverified', 'failed'] : ['Verified', 'success'] + = link_to domain.url, title: tooltip, class: 'has-tooltip' do + = sprite_icon("status_#{status}", size: 16, css_class: "has-tooltip ci-status-icon ci-status-icon-#{status}") + = domain.domain + - else + = link_to domain.domain, domain.url %p - if domain.subject %span.label.label-gray Certificate: #{domain.subject} - if domain.expired? %span.label.label-danger Expired + - if verification_enabled && domain.unverified? + %li.warning-row + #{domain.domain} is not verified. To learn how to verify ownership, visit your + = link_to 'domain details', project_pages_domain_path(@project, domain) diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index 876cac0dacb..72e9203bdb0 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -1,4 +1,10 @@ - page_title "#{@domain.domain}", 'Pages Domains' +- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? +- if verification_enabled && @domain.unverified? + %p.alert.alert-warning + %strong + This domain is not verified. You will need to verify ownership before + access is enabled. %h3.page-title Pages Domain @@ -15,9 +21,26 @@ DNS %td %p - To access the domain create a new DNS record: + To access this domain create a new DNS record: %pre #{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}. + - if verification_enabled + %tr + %td + Verification status + %td + %p + - help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + To #{link_to 'verify ownership', help_link} of your domain, create + this DNS record: + %pre + #{@domain.verification_domain} TXT #{@domain.keyed_verification_code} + %p + - if @domain.verified? + #{@domain.domain} has been successfully verified. + - else + = button_to 'Verify ownership', verify_project_pages_domain_path(@project, @domain), class: 'btn btn-save btn-sm' + %tr %td Certificate diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index b037b57e78a..4fd4ca355a8 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -1,6 +1,6 @@ %h3 Shared Runners -.bs-callout.bs-callout-warning.shared-runners-description +.bs-callout.shared-runners-description - if Gitlab::CurrentSettings.shared_runners_text.present? = markdown_field(Gitlab::CurrentSettings.current_application_settings, :shared_runners_text) - else @@ -9,7 +9,7 @@ on GitLab.com). %hr - if @project.shared_runners_enabled? - = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-warning', method: :post do + = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do Disable shared Runners - else = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 28ccbf7eb15..f0813e56b71 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -1,8 +1,7 @@ %h3 Specific Runners -= render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: @project.runners_token, - type: 'specific' } += render partial: 'ci/runner/how_to_setup_specific_runner', + locals: { registration_token: @project.runners_token } - if @project_runners.any? %h4.underlined-title Runners activated for this project diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml index 5f38ecd6820..6dc2b85fd32 100644 --- a/app/views/projects/services/prometheus/_show.html.haml +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -7,7 +7,7 @@ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus') .col-lg-9 - .panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } } + .panel.panel-default.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json) } } .panel-heading %h3.panel-title = s_('PrometheusService|Monitored') diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 3077203c2a6..235d532bf98 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -4,7 +4,6 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('deploy_keys') -# Protected branches & tags use a lot of nested partials. -# The shared parts of the views can be found in the `shared` directory. diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 888d820b04e..fa281327eb7 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,6 +1,7 @@ - @no_container = true - breadcrumb_title "Details" - @content_class = "limit-container-width" unless fluid_layout +- show_auto_devops_callout = show_auto_devops_callout?(@project) = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") @@ -14,65 +15,9 @@ - if can?(current_user, :download_code, @project) %nav.project-stats{ class: container_class } - %ul.nav - %li - = link_to project_tree_path(@project) do - #{_('Files')} (#{storage_counter(@project.statistics.total_repository_size)}) - %li - = link_to project_commits_path(@project, current_ref) do - #{n_('Commit', 'Commits', @project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %li - = link_to project_branches_path(@project) do - #{n_('Branch', 'Branches', @repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) - %li - = link_to project_tags_path(@project) do - #{n_('Tag', 'Tags', @repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) + = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) + = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) - - if @repository.readme - %li - = link_to _('Readme'), - default_project_view != 'readme' ? readme_path(@project) : '#readme' - - - if @repository.changelog - %li - = link_to _('Changelog'), changelog_path(@project) - - - if @repository.license_blob - %li - = link_to license_short_name(@project), license_path(@project) - - - if @repository.contribution_guide - %li - = link_to _('Contribution guide'), contribution_guide_path(@project) - - - if @repository.gitlab_ci_yml - %li - = link_to _('CI/CD configuration'), ci_configuration_path(@project) - - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - #{ _('Add Changelog') } - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - #{ _('Add License') } - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - #{ _('Add Contribution guide') } - - unless @repository.gitlab_ci_yml - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - #{ _('Set up CI/CD') } - - if koding_enabled? && @repository.koding_yml.blank? - %li.missing - = link_to _('Set up Koding'), add_koding_stack_path(@project) - - if @repository.gitlab_ci_yml.blank? && @project.deployment_platform.present? - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do - #{ _('Set up auto deploy') } %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - if @project.archived? @@ -81,7 +26,7 @@ = icon("exclamation-triangle fw") #{ _('Archived project! Repository is read-only') } - - view_path = default_project_view + - view_path = @project.default_view - if show_auto_devops_callout?(@project) = render 'shared/auto_devops_callout' diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 05539dfed7c..39511435508 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -75,7 +75,7 @@ - if show_new_ide? = succeed " " do = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do - = ide_edit_text + = _('Web IDE') = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index a10fc42b82d..3312254f5fb 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -6,7 +6,6 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'boards' %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 79021a08719..6dfabd7ba4c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -69,7 +69,7 @@ - else = form.submit 'Save changes', class: 'btn btn-save' - - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) + - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = issuable.project.present.contribution_guide_path) .inline.prepend-top-10 Please review the %strong= link_to('contribution guidelines', guide_url) diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 33435216c14..0687f6d961d 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -6,7 +6,7 @@ - user = local_assigns[:user] - access = user&.max_member_access_for_project(project.id) unless user.nil? - css_class = '' unless local_assigns[:css_class] -- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit +- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project) - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - cache_key = project_list_cache_key(project) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date) @@ -47,7 +47,7 @@ .prepend-top-0 - if project.archived %span.prepend-left-10.label.label-warning archived - - if project.pipeline_status.has_status? + - if can?(current_user, :read_cross_project) && project.pipeline_status.has_status? %span.prepend-left-10 = render_project_pipeline_status(project.pipeline_status) - if forks diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index a396d1007a7..4bf01ecb48c 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -82,47 +82,58 @@ .fade-left= icon('angle-left') .fade-right= icon('angle-right') %ul.nav-links.user-profile-nav.scrolling-tabs - %li.js-activity-tab - = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do - Activity - %li.js-groups-tab - = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do - Groups - %li.js-contributed-tab - = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do - Contributed projects - %li.js-projects-tab - = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do - Personal projects - %li.js-snippets-tab - = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do - Snippets + - if profile_tab?(:activity) + %li.js-activity-tab + = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do + Activity + - if profile_tab?(:groups) + %li.js-groups-tab + = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do + Groups + - if profile_tab?(:contributed) + %li.js-contributed-tab + = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do + Contributed projects + - if profile_tab?(:projects) + %li.js-projects-tab + = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do + Personal projects + - if profile_tab?(:snippets) + %li.js-snippets-tab + = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do + Snippets %div{ class: container_class } .tab-content - #activity.tab-pane - .row-content-block.calender-block.white.second-block.hidden-xs - .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } - %h4.center.light - %i.fa.fa-spinner.fa-spin - .user-calendar-activities + - if profile_tab?(:activity) + #activity.tab-pane + .row-content-block.calender-block.white.second-block.hidden-xs + .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } + %h4.center.light + %i.fa.fa-spinner.fa-spin + .user-calendar-activities - %h4.prepend-top-20 - Most Recent Activity - .content_list{ data: { href: user_path } } - = spinner + - if can?(current_user, :read_cross_project) + %h4.prepend-top-20 + Most Recent Activity + .content_list{ data: { href: user_path } } + = spinner - #groups.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:groups) + #groups.tab-pane + -# This tab is always loaded via AJAX - #contributed.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:contributed) + #contributed.tab-pane + -# This tab is always loaded via AJAX - #projects.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:projects) + #projects.tab-pane + -# This tab is always loaded via AJAX - #snippets.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:snippets) + #snippets.tab-pane + -# This tab is always loaded via AJAX .loading-status = spinner diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f2c20114534..28a5e5da037 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -3,6 +3,7 @@ - cronjob:expire_build_artifacts - cronjob:gitlab_usage_ping - cronjob:import_export_project_cleanup +- cronjob:pages_domain_verification_cron - cronjob:pipeline_schedule - cronjob:prune_old_events - cronjob:remove_expired_group_links @@ -82,6 +83,7 @@ - new_merge_request - new_note - pages +- pages_domain_verification - post_receive - process_commit - project_cache diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 09559e3b696..d7e24491516 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -1,42 +1,10 @@ class AuthorizedProjectsWorker include ApplicationWorker + prepend WaitableWorker - # Schedules multiple jobs and waits for them to be completed. - def self.bulk_perform_and_wait(args_list) - # Short-circuit: it's more efficient to do small numbers of jobs inline - return bulk_perform_inline(args_list) if args_list.size <= 3 - - waiter = Gitlab::JobWaiter.new(args_list.size) - - # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]] - # into [[1, "key"], [2, "key"], [3, "key"]] - waiting_args_list = args_list.map { |args| [*args, waiter.key] } - bulk_perform_async(waiting_args_list) - - waiter.wait - end - - # Performs multiple jobs directly. Failed jobs will be put into sidekiq so - # they can benefit from retries - def self.bulk_perform_inline(args_list) - failed = [] - - args_list.each do |args| - begin - new.perform(*args) - rescue - failed << args - end - end - - bulk_perform_async(failed) if failed.present? - end - - def perform(user_id, notify_key = nil) + def perform(user_id) user = User.find_by(id: user_id) user&.refresh_authorized_projects - ensure - Gitlab::JobWaiter.notify(notify_key, jid) if notify_key end end diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb new file mode 100644 index 00000000000..48ebe862248 --- /dev/null +++ b/app/workers/concerns/waitable_worker.rb @@ -0,0 +1,44 @@ +module WaitableWorker + extend ActiveSupport::Concern + + module ClassMethods + # Schedules multiple jobs and waits for them to be completed. + def bulk_perform_and_wait(args_list, timeout: 10) + # Short-circuit: it's more efficient to do small numbers of jobs inline + return bulk_perform_inline(args_list) if args_list.size <= 3 + + waiter = Gitlab::JobWaiter.new(args_list.size) + + # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]] + # into [[1, "key"], [2, "key"], [3, "key"]] + waiting_args_list = args_list.map { |args| [*args, waiter.key] } + bulk_perform_async(waiting_args_list) + + waiter.wait(timeout) + end + + # Performs multiple jobs directly. Failed jobs will be put into sidekiq so + # they can benefit from retries + def bulk_perform_inline(args_list) + failed = [] + + args_list.each do |args| + begin + new.perform(*args) + rescue + failed << args + end + end + + bulk_perform_async(failed) if failed.present? + end + end + + def perform(*args) + notify_key = args.pop if Gitlab::JobWaiter.key?(args.last) + + super(*args) + ensure + Gitlab::JobWaiter.notify(notify_key, jid) if notify_key + end +end diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb new file mode 100644 index 00000000000..a3ff4bd2101 --- /dev/null +++ b/app/workers/pages_domain_verification_cron_worker.rb @@ -0,0 +1,10 @@ +class PagesDomainVerificationCronWorker + include ApplicationWorker + include CronjobQueue + + def perform + PagesDomain.needs_verification.find_each do |domain| + PagesDomainVerificationWorker.perform_async(domain.id) + end + end +end diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb new file mode 100644 index 00000000000..2e93489113c --- /dev/null +++ b/app/workers/pages_domain_verification_worker.rb @@ -0,0 +1,11 @@ +class PagesDomainVerificationWorker + include ApplicationWorker + + def perform(domain_id) + domain = PagesDomain.find_by(id: domain_id) + + return unless domain + + VerifyPagesDomainService.new(domain).execute + end +end diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb index e0e6d1418de..fbb14efc525 100644 --- a/app/workers/stuck_import_jobs_worker.rb +++ b/app/workers/stuck_import_jobs_worker.rb @@ -16,43 +16,41 @@ class StuckImportJobsWorker private def mark_projects_without_jid_as_failed! - started_projects_without_jid.each do |project| + enqueued_projects_without_jid.each do |project| project.mark_import_as_failed(error_message) end.count end def mark_projects_with_jid_as_failed! - completed_jids_count = 0 + jids_and_ids = enqueued_projects_with_jid.pluck(:import_jid, :id).to_h - started_projects_with_jid.find_in_batches(batch_size: 500) do |group| - jids = group.map(&:import_jid) + # Find the jobs that aren't currently running or that exceeded the threshold. + completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys) + return unless completed_jids.any? - # Find the jobs that aren't currently running or that exceeded the threshold. - completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set + completed_project_ids = jids_and_ids.values_at(*completed_jids) - if completed_jids.any? - completed_jids_count += completed_jids.count - group.each do |project| - project.mark_import_as_failed(error_message) if completed_jids.include?(project.import_jid) - end + # We select the projects again, because they may have transitioned from + # scheduled/started to finished/failed while we were looking up their Sidekiq status. + completed_projects = enqueued_projects_with_jid.where(id: completed_project_ids) - Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.to_a.join(', ')}") - end - end + Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_projects.map(&:import_jid).join(', ')}") - completed_jids_count + completed_projects.each do |project| + project.mark_import_as_failed(error_message) + end.count end - def started_projects - Project.with_import_status(:started) + def enqueued_projects + Project.with_import_status(:scheduled, :started) end - def started_projects_with_jid - started_projects.where.not(import_jid: nil) + def enqueued_projects_with_jid + enqueued_projects.where.not(import_jid: nil) end - def started_projects_without_jid - started_projects.where(import_jid: nil) + def enqueued_projects_without_jid + enqueued_projects.where(import_jid: nil) end def error_message |