diff options
Diffstat (limited to 'app')
228 files changed, 2773 insertions, 1274 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 4f63c7988f5..67106e85a37 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward( this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); return typeof callback === 'function' ? callback() : undefined; }); - return $('.emoji-menu').removeClass('is-visible'); + $('.emoji-menu').removeClass('is-visible'); + $('.js-add-award.is-active').removeClass('is-active'); }; AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js new file mode 100644 index 00000000000..aa9a4e1c99a --- /dev/null +++ b/app/assets/javascripts/blob/blob_fork_suggestion.js @@ -0,0 +1,15 @@ +function BlobForkSuggestion(openButton, cancelButton, suggestionSection) { + if (openButton) { + openButton.addEventListener('click', () => { + suggestionSection.classList.remove('hidden'); + }); + } + + if (cancelButton) { + cancelButton.addEventListener('click', () => { + suggestionSection.classList.add('hidden'); + }); + } +} + +export default BlobForkSuggestion; diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index a4629b092bf..e48d3344a2b 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -20,6 +20,7 @@ import eventHub from '../eventhub'; list: { type: Object, required: false, + default: () => ({}), }, rootPath: { type: String, @@ -31,6 +32,26 @@ import eventHub from '../eventhub'; default: false, }, }, + computed: { + cardUrl() { + return `${this.issueLinkBase}/${this.issue.id}`; + }, + assigneeUrl() { + return `${this.rootPath}${this.issue.assignee.username}`; + }, + assigneeUrlTitle() { + return `Assigned to ${this.issue.assignee.name}`; + }, + avatarUrlTitle() { + return `Avatar for ${this.issue.assignee.name}`; + }, + issueId() { + return `#${this.issue.id}`; + }, + showLabelFooter() { + return this.issue.labels.find(l => this.showLabel(l)) !== undefined; + }, + }, methods: { showLabel(label) { if (!this.list) return true; @@ -67,35 +88,41 @@ import eventHub from '../eventhub'; }, template: ` <div> - <h4 class="card-title"> - <i - class="fa fa-eye-slash confidential-icon" - v-if="issue.confidential"></i> - <a - :href="issueLinkBase + '/' + issue.id" - :title="issue.title"> - {{ issue.title }} - </a> - </h4> - <div class="card-footer"> - <span - class="card-number" - v-if="issue.id"> - #{{ issue.id }} - </span> + <div class="card-header"> + <h4 class="card-title"> + <i + class="fa fa-eye-slash confidential-icon" + v-if="issue.confidential" + aria-hidden="true" + /> + <a + class="js-no-trigger" + :href="cardUrl" + :title="issue.title">{{ issue.title }}</a> + <span + class="card-number" + v-if="issue.id" + > + {{ issueId }} + </span> + </h4> <a class="card-assignee has-tooltip js-no-trigger" - :href="rootPath + issue.assignee.username" - :title="'Assigned to ' + issue.assignee.name" + :href="assigneeUrl" + :title="assigneeUrlTitle" v-if="issue.assignee" - data-container="body"> + data-container="body" + > <img class="avatar avatar-inline s20 js-no-trigger" :src="issue.assignee.avatar" width="20" height="20" - :alt="'Avatar for ' + issue.assignee.name" /> + :alt="avatarUrlTitle" + /> </a> + </div> + <div class="card-footer" v-if="showLabelFooter"> <button class="label color-label has-tooltip js-no-trigger" v-for="label in issue.labels" diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index fe54ecffdfe..7cb022c848a 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -84,10 +84,10 @@ window.Build = (function() { var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; return $.ajax({ - url: this.buildUrl, + url: this.pageUrl + "/trace.json", dataType: 'json', success: function(buildData) { - $('.js-build-output').html(buildData.trace_html); + $('.js-build-output').html(buildData.html); gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); if (window.location.hash === DOWN_BUILD_TRACE) { $("html,body").scrollTop(this.$buildTrace.height()); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index a92e068ca5a..5f3ed9374bf 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -8,10 +8,8 @@ Vue.use(VueResource); /** * Commits View > Pipelines Tab > Pipelines Table. - * Merge Request View > Pipelines Tab > Pipelines Table. * * Renders Pipelines table in pipelines tab in the commits show view. - * Renders Pipelines table in pipelines tab in the merge request show view. */ $(() => { @@ -20,13 +18,14 @@ $(() => { gl.commits.pipelines = gl.commits.pipelines || {}; if (gl.commits.PipelinesTableBundle) { + document.querySelector('#commit-pipeline-table-view').removeChild(this.pipelinesTableBundle.$el); gl.commits.PipelinesTableBundle.$destroy(true); } const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable(); if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { - gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); + gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); + document.querySelector('#commit-pipeline-table-view').appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); } }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 4d5a857d705..da9707549f9 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import Visibility from 'visibilityjs'; import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; @@ -7,6 +8,7 @@ import EmptyState from '../../vue_pipelines_index/components/empty_state'; import ErrorState from '../../vue_pipelines_index/components/error_state'; import '../../lib/utils/common_utils'; import '../../vue_shared/vue_resource_interceptor'; +import Poll from '../../lib/utils/poll'; /** * @@ -20,6 +22,7 @@ import '../../vue_shared/vue_resource_interceptor'; */ export default Vue.component('pipelines-table', { + components: { 'pipelines-table-component': PipelinesTableComponent, 'error-state': ErrorState, @@ -42,6 +45,7 @@ export default Vue.component('pipelines-table', { state: store.state, isLoading: false, hasError: false, + isMakingRequest: false, }; }, @@ -64,17 +68,41 @@ export default Vue.component('pipelines-table', { * */ beforeMount() { - this.endpoint = this.$el.dataset.endpoint; - this.helpPagePath = this.$el.dataset.helpPagePath; + const element = document.querySelector('#commit-pipeline-table-view'); + + this.endpoint = element.dataset.endpoint; + this.helpPagePath = element.dataset.helpPagePath; this.service = new PipelinesService(this.endpoint); - this.fetchPipelines(); + this.poll = new Poll({ + resource: this.service, + method: 'getPipelines', + successCallback: this.successCallback, + errorCallback: this.errorCallback, + notificationCallback: this.setIsMakingRequest, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); eventHub.$on('refreshPipelines', this.fetchPipelines); }, beforeUpdate() { - if (this.state.pipelines.length && this.$children) { + if (this.state.pipelines.length && + this.$children && + !this.isMakingRequest && + !this.isLoading) { this.store.startTimeAgoLoops.call(this, Vue); } }, @@ -83,21 +111,35 @@ export default Vue.component('pipelines-table', { eventHub.$off('refreshPipelines'); }, + destroyed() { + this.poll.stop(); + }, + methods: { fetchPipelines() { this.isLoading = true; + return this.service.getPipelines() - .then(response => response.json()) - .then((json) => { - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = json.pipelines || json; - this.store.storePipelines(pipelines); - this.isLoading = false; - }) - .catch(() => { - this.hasError = true; - this.isLoading = false; - }); + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + }, + + successCallback(resp) { + const response = resp.json(); + + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = response.pipelines || response; + this.store.storePipelines(pipelines); + this.isLoading = false; + }, + + errorCallback() { + this.hasError = true; + this.isLoading = false; + }, + + setIsMakingRequest(isMakingRequest) { + this.isMakingRequest = isMakingRequest; }, }, diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 6dbec50b890..ab9a8e43dd1 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -38,9 +38,35 @@ showTooltip = function(target, title) { }; $(function() { - var clipboard; - - clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); + const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); clipboard.on('success', genericSuccess); - return clipboard.on('error', genericError); + clipboard.on('error', genericError); + + // This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM. + // The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and `gfm` keys into the `data-clipboard-text` + // attribute that ClipboardJS reads from. + // When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value + // to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command, + // this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the + // `text/plain` and `text/x-gfm` copy data types to the intended values. + $(document).on('copy', 'body > textarea[readonly]', function(e) { + const clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; + + const text = e.target.value; + + let json; + try { + json = JSON.parse(text); + } catch (ex) { + return; + } + + if (!json.text || !json.gfm) return; + + e.preventDefault(); + + clipboardData.setData('text/plain', json.text); + clipboardData.setData('text/x-gfm', json.gfm); + }); }); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 88180149715..5aa3eb46a69 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -13,10 +13,6 @@ class Diff { $diffFile.each((index, file) => new gl.ImageFile(file)); - if (this.diffViewType() === 'parallel') { - $('.content-wrapper .container-fluid').removeClass('container-limited'); - } - if (!isBound) { $(document) .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 9c7acc903d1..0a1a62fb012 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -24,7 +24,6 @@ /* global Search */ /* global Admin */ /* global NamespaceSelects */ -/* global ShortcutsDashboardNavigation */ /* global Project */ /* global ProjectAvatar */ /* global CompareAutocomplete */ @@ -43,6 +42,7 @@ import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; +import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; const ShortcutsBlob = require('./shortcuts_blob'); @@ -86,6 +86,12 @@ const ShortcutsBlob = require('./shortcuts_blob'); skipResetBindings: true, fileBlobPermalinkUrl, }); + + new BlobForkSuggestion( + document.querySelector('.js-edit-blob-link-fork-toggler'), + document.querySelector('.js-cancel-fork-suggestion'), + document.querySelector('.js-file-fork-suggestion-section'), + ); } switch (page) { @@ -371,7 +377,6 @@ const ShortcutsBlob = require('./shortcuts_blob'); break; case 'dashboard': case 'root': - shortcut_handler = new ShortcutsDashboardNavigation(); new UserCallout(); break; case 'groups': diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js index 8abbcf0c227..d2514593e3a 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.js +++ b/app/assets/javascripts/environments/folder/environments_folder_view.js @@ -31,12 +31,6 @@ export default Vue.component('environment-folder-view', { cssContainerClass: environmentsData.cssClass, canCreateDeployment: environmentsData.canCreateDeployment, canReadEnvironment: environmentsData.canReadEnvironment, - - // svgs - commitIconSvg: environmentsData.commitIconSvg, - playIconSvg: environmentsData.playIconSvg, - terminalIconSvg: environmentsData.terminalIconSvg, - // Pagination Properties, paginationInformation: {}, pageNumber: 1, @@ -163,9 +157,6 @@ export default Vue.component('environment-folder-view', { :environments="state.environments" :can-create-deployment="canCreateDeploymentParsed" :can-read-environment="canReadEnvironmentParsed" - :play-icon-svg="playIconSvg" - :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg" :service="service"/> <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 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 new file mode 100644 index 00000000000..9126422b335 --- /dev/null +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js @@ -0,0 +1,87 @@ +import eventHub from '../event_hub'; + +export default { + name: 'RecentSearchesDropdownContent', + + props: { + items: { + type: Array, + required: true, + }, + }, + + computed: { + processedItems() { + return this.items.map((item) => { + const { tokens, searchToken } + = gl.FilteredSearchTokenizer.processTokens(item); + + 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> + <ul v-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/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index ede5d65ce12..381c40c03d8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -55,7 +55,8 @@ require('./filtered_search_dropdown'); renderContent() { const dropdownData = []; - [].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { + + [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { const { icon, hint, tag, type } = dropdownMenu.dataset; if (icon && hint && tag) { dropdownData.push( diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 432b0c0dfd2..6c5c20447f7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -129,7 +129,9 @@ import FilteredSearchContainer from './container'; } }); - return values.join(' '); + return values + .map(value => value.trim()) + .join(' '); } static getSearchInput(filteredSearchInput) { diff --git a/app/assets/javascripts/filtered_search/event_hub.js b/app/assets/javascripts/filtered_search/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/filtered_search/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 36090b49651..b93a8f1d322 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,18 +1,56 @@ +/* global Flash */ + import FilteredSearchContainer from './container'; +import RecentSearchesRoot from './recent_searches_root'; +import RecentSearchesStore from './stores/recent_searches_store'; +import RecentSearchesService from './services/recent_searches_service'; +import eventHub from './event_hub'; (() => { class FilteredSearchManager { constructor(page) { this.container = FilteredSearchContainer.container; this.filteredSearchInput = this.container.querySelector('.filtered-search'); + this.filteredSearchInputForm = this.filteredSearchInput.form; this.clearSearchButton = this.container.querySelector('.clear-search'); this.tokensContainer = this.container.querySelector('.tokens-container'); this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; + this.recentSearchesStore = new RecentSearchesStore(); + let recentSearchesKey = 'issue-recent-searches'; + if (page === 'merge_requests') { + recentSearchesKey = 'merge-request-recent-searches'; + } + this.recentSearchesService = new RecentSearchesService(recentSearchesKey); + + // Fetch recent searches from localStorage + this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() + .catch(() => { + // eslint-disable-next-line no-new + new Flash('An error occured while parsing recent searches'); + // Gracefully fail to empty array + return []; + }) + .then((searches) => { + // Put any searches that may have come in before + // we fetched the saved searches ahead of the already saved ones + const resultantSearches = this.recentSearchesStore.setRecentSearches( + this.recentSearchesStore.state.recentSearches.concat(searches), + ); + this.recentSearchesService.save(resultantSearches); + }); + if (this.filteredSearchInput) { this.tokenizer = gl.FilteredSearchTokenizer; this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page); + this.recentSearchesRoot = new RecentSearchesRoot( + this.recentSearchesStore, + this.recentSearchesService, + document.querySelector('.js-filtered-search-history-dropdown'), + ); + this.recentSearchesRoot.init(); + this.bindEvents(); this.loadSearchParamsFromURL(); this.dropdownManager.setDropdown(); @@ -25,6 +63,10 @@ import FilteredSearchContainer from './container'; cleanup() { this.unbindEvents(); document.removeEventListener('beforeunload', this.cleanupWrapper); + + if (this.recentSearchesRoot) { + this.recentSearchesRoot.destroy(); + } } bindEvents() { @@ -34,7 +76,7 @@ import FilteredSearchContainer from './container'; this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this); this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this); - this.clearSearchWrapper = this.clearSearch.bind(this); + this.onClearSearchWrapper = this.onClearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); @@ -42,8 +84,8 @@ import FilteredSearchContainer from './container'; this.tokenChange = this.tokenChange.bind(this); this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); + this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); - this.filteredSearchInputForm = this.filteredSearchInput.form; this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); @@ -56,11 +98,12 @@ import FilteredSearchContainer from './container'; this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); - this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('keydown', this.removeSelectedTokenWrapper); + eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); } unbindEvents() { @@ -76,11 +119,12 @@ import FilteredSearchContainer from './container'; this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); - this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); + this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('keydown', this.removeSelectedTokenWrapper); + eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); } checkForBackspace(e) { @@ -131,7 +175,7 @@ import FilteredSearchContainer from './container'; } addInputContainerFocus() { - const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container'); + const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); if (inputContainer) { inputContainer.classList.add('focus'); @@ -139,7 +183,7 @@ import FilteredSearchContainer from './container'; } removeInputContainerFocus(e) { - const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container'); + const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; @@ -161,7 +205,7 @@ import FilteredSearchContainer from './container'; } unselectEditTokens(e) { - const inputContainer = this.container.querySelector('.filtered-search-input-container'); + const inputContainer = this.container.querySelector('.filtered-search-box'); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementTokensContainer = e.target.classList.contains('tokens-container'); @@ -215,9 +259,12 @@ import FilteredSearchContainer from './container'; } } - clearSearch(e) { + onClearSearch(e) { e.preventDefault(); + this.clearSearch(); + } + clearSearch() { this.filteredSearchInput.value = ''; const removeElements = []; @@ -289,6 +336,17 @@ import FilteredSearchContainer from './container'; this.search(); } + saveCurrentSearchQuery() { + // Don't save before we have fetched the already saved searches + this.fetchingRecentSearchesPromise.then(() => { + const searchQuery = gl.DropdownUtils.getSearchQuery(); + if (searchQuery.length > 0) { + const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); + this.recentSearchesService.save(resultantSearches); + } + }); + } + loadSearchParamsFromURL() { const params = gl.utils.getUrlParamsArray(); const usernameParams = this.getUsernameParams(); @@ -343,6 +401,8 @@ import FilteredSearchContainer from './container'; } }); + this.saveCurrentSearchQuery(); + if (hasFilteredSearch) { this.clearSearchButton.classList.remove('hidden'); this.handleInputPlaceholder(); @@ -351,8 +411,12 @@ import FilteredSearchContainer from './container'; search() { const paths = []; + const searchQuery = gl.DropdownUtils.getSearchQuery(); + + this.saveCurrentSearchQuery(); + const { tokens, searchToken } - = this.tokenizer.processTokens(gl.DropdownUtils.getSearchQuery()); + = this.tokenizer.processTokens(searchQuery); const currentState = gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); @@ -416,6 +480,13 @@ import FilteredSearchContainer from './container'; currentDropdownRef.dispatchInputEvent(); } } + + onrecentSearchesItemSelected(text) { + this.clearSearch(); + this.filteredSearchInput.value = text; + this.filteredSearchInput.dispatchEvent(new CustomEvent('input')); + this.search(); + } } window.gl = window.gl || {}; diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js new file mode 100644 index 00000000000..4e38409e12a --- /dev/null +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -0,0 +1,59 @@ +import Vue from 'vue'; +import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content'; +import eventHub from './event_hub'; + +class RecentSearchesRoot { + constructor( + recentSearchesStore, + recentSearchesService, + wrapperElement, + ) { + this.store = recentSearchesStore; + this.service = recentSearchesService; + this.wrapperElement = wrapperElement; + } + + init() { + this.bindEvents(); + this.render(); + } + + bindEvents() { + this.onRequestClearRecentSearchesWrapper = this.onRequestClearRecentSearches.bind(this); + + eventHub.$on('requestClearRecentSearches', this.onRequestClearRecentSearchesWrapper); + } + + unbindEvents() { + eventHub.$off('requestClearRecentSearches', this.onRequestClearRecentSearchesWrapper); + } + + render() { + this.vm = new Vue({ + el: this.wrapperElement, + data: this.store.state, + template: ` + <recent-searches-dropdown-content + :items="recentSearches" /> + `, + components: { + 'recent-searches-dropdown-content': RecentSearchesDropdownContent, + }, + }); + } + + onRequestClearRecentSearches() { + const resultantSearches = this.store.setRecentSearches([]); + this.service.save(resultantSearches); + } + + destroy() { + this.unbindEvents(); + if (this.vm) { + this.vm.$destroy(); + } + } + +} + +export default RecentSearchesRoot; diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service.js b/app/assets/javascripts/filtered_search/services/recent_searches_service.js new file mode 100644 index 00000000000..3e402d5aed0 --- /dev/null +++ b/app/assets/javascripts/filtered_search/services/recent_searches_service.js @@ -0,0 +1,26 @@ +class RecentSearchesService { + constructor(localStorageKey = 'issuable-recent-searches') { + this.localStorageKey = localStorageKey; + } + + fetch() { + const input = window.localStorage.getItem(this.localStorageKey); + + let searches = []; + if (input && input.length > 0) { + try { + searches = JSON.parse(input); + } catch (err) { + return Promise.reject(err); + } + } + + return Promise.resolve(searches); + } + + save(searches = []) { + window.localStorage.setItem(this.localStorageKey, JSON.stringify(searches)); + } +} + +export default RecentSearchesService; diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js new file mode 100644 index 00000000000..066be69766a --- /dev/null +++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js @@ -0,0 +1,23 @@ +import _ from 'underscore'; + +class RecentSearchesStore { + constructor(initialState = {}) { + this.state = Object.assign({ + recentSearches: [], + }, initialState); + } + + addRecentSearch(newSearch) { + this.setRecentSearches([newSearch].concat(this.state.recentSearches)); + + return this.state.recentSearches; + } + + setRecentSearches(searches = []) { + const trimmedSearches = searches.map(search => search.trim()); + this.state.recentSearches = _.uniq(trimmedSearches).slice(0, 5); + return this.state.recentSearches; + } +} + +export default RecentSearchesStore; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 5c22aea51cd..e31cc5fbabe 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -65,7 +65,6 @@ export default class Poll { this.makeRequest(); }, pollInterval); } - this.options.successCallback(response); } @@ -76,8 +75,14 @@ export default class Poll { notificationCallback(true); return resource[method](data) - .then(response => this.checkConditions(response)) - .catch(error => errorCallback(error)); + .then((response) => { + this.checkConditions(response); + notificationCallback(false); + }) + .catch((error) => { + notificationCallback(false); + errorCallback(error); + }); } /** diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3c4e6102469..8528e0800ae 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -90,6 +90,7 @@ import './flash'; .on('click', this.clickTab); } + // Used in tests unbindEvents() { $(document) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) @@ -99,9 +100,11 @@ import './flash'; .off('click', this.clickTab); } - destroy() { - this.unbindEvents(); + destroyPipelinesView() { if (this.commitPipelinesTable) { + document.querySelector('#commit-pipeline-table-view') + .removeChild(this.commitPipelinesTable.$el); + this.commitPipelinesTable.$destroy(); } } @@ -128,6 +131,7 @@ import './flash'; this.loadCommits($target.attr('href')); this.expandView(); this.resetViewContainer(); + this.destroyPipelinesView(); } else if (this.isDiffAction(action)) { this.loadDiff($target.attr('href')); if (Breakpoints.get().getBreakpointSize() !== 'lg') { @@ -136,12 +140,14 @@ import './flash'; if (this.diffViewType() === 'parallel') { this.expandViewContainer(); } + this.destroyPipelinesView(); } else if (action === 'pipelines') { this.resetViewContainer(); - this.loadPipelines(); + this.mountPipelinesView(); } else { this.expandView(); this.resetViewContainer(); + this.destroyPipelinesView(); } if (this.setUrl) { this.setCurrentAction(action); @@ -227,16 +233,12 @@ import './flash'; }); } - loadPipelines() { - if (this.pipelinesLoaded) { - return; - } - const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - // Could already be mounted from the `pipelines_bundle` - if (pipelineTableViewEl) { - this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl); - } - this.pipelinesLoaded = true; + mountPipelinesView() { + this.commitPipelinesTable = new CommitPipelinesTable().$mount(); + // $mount(el) replaces the el with the new rendered component. We need it in order to mount + // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount + document.querySelector('#commit-pipeline-table-view') + .appendChild(this.commitPipelinesTable.$el); } loadDiff(source) { diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index a6ffa0f59de..d82a4eb9642 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status'; import { formatRelevantDigits } from '~/lib/utils/number_utils'; import '../flash'; +const prometheusContainer = '.prometheus-container'; +const prometheusParentGraphContainer = '.prometheus-graphs'; const prometheusGraphsContainer = '.prometheus-graph'; +const prometheusStatesContainer = '.prometheus-state'; const metricsEndpoint = 'metrics.json'; const timeFormat = d3.time.format('%H:%M'); const dayFormat = d3.time.format('%b %e, %a'); @@ -14,19 +17,30 @@ const bisectDate = d3.bisector(d => d.time).left; const extraAddedWidthParent = 100; class PrometheusGraph { - constructor() { - this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; - this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; - const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + - extraAddedWidthParent; - this.originalWidth = parentContainerWidth; - this.originalHeight = 330; - this.width = parentContainerWidth - this.margin.left - this.margin.right; - this.height = this.originalHeight - this.margin.top - this.margin.bottom; - this.backOffRequestCounter = 0; - this.configureGraph(); - this.init(); + const $prometheusContainer = $(prometheusContainer); + const hasMetrics = $prometheusContainer.data('has-metrics'); + this.docLink = $prometheusContainer.data('doc-link'); + this.integrationLink = $prometheusContainer.data('prometheus-integration'); + + $(document).ajaxError(() => {}); + + if (hasMetrics) { + this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; + this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; + const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + + extraAddedWidthParent; + this.originalWidth = parentContainerWidth; + this.originalHeight = 330; + this.width = parentContainerWidth - this.margin.left - this.margin.right; + this.height = this.originalHeight - this.margin.top - this.margin.bottom; + this.backOffRequestCounter = 0; + this.configureGraph(); + this.init(); + } else { + this.state = '.js-getting-started'; + this.updateState(); + } } createGraph() { @@ -40,8 +54,19 @@ class PrometheusGraph { init() { this.getData().then((metricsResponse) => { - if (Object.keys(metricsResponse).length === 0) { - new Flash('Empty metrics', 'alert'); + let enoughData = true; + Object.keys(metricsResponse.metrics).forEach((key) => { + let currentKey; + if (key === 'cpu_values' || key === 'memory_values') { + currentKey = metricsResponse.metrics[key]; + if (Object.keys(currentKey).length === 0) { + enoughData = false; + } + } + }); + if (!enoughData) { + this.state = '.js-loading'; + this.updateState(); } else { this.transformData(metricsResponse); this.createGraph(); @@ -345,14 +370,17 @@ class PrometheusGraph { } return resp.metrics; }) - .catch(() => new Flash('An error occurred while fetching metrics.', 'alert')); + .catch(() => { + this.state = '.js-unable-to-connect'; + this.updateState(); + }); } transformData(metricsResponse) { Object.keys(metricsResponse.metrics).forEach((key) => { if (key === 'cpu_values' || key === 'memory_values') { const metricValues = (metricsResponse.metrics[key])[0]; - if (typeof metricValues !== 'undefined') { + if (metricValues !== undefined) { this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ time: new Date(metric[0] * 1000), value: metric[1], @@ -361,6 +389,13 @@ class PrometheusGraph { } }); } + + updateState() { + const $statesContainer = $(prometheusStatesContainer); + $(prometheusParentGraphContainer).hide(); + $(`${this.state}`, $statesContainer).removeClass('hidden'); + $(prometheusStatesContainer).show(); + } } export default PrometheusGraph; diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index fd5097696ad..5b6bb2bf3f5 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* global Mousetrap */ /* global findFileURL */ +import findAndFollowLink from './shortcuts_dashboard_navigation'; (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -14,11 +15,33 @@ } Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('s', Shortcuts.focusSearch); - Mousetrap.bind('f', (function(_this) { - return function(e) { - return _this.focusFilter(e); - }; - })(this)); + Mousetrap.bind('f', (e => this.focusFilter(e))); + + const $globalDropdownMenu = $('.global-dropdown-menu'); + const $globalDropdownToggle = $('.global-dropdown-toggle'); + + $('.global-dropdown').on('hide.bs.dropdown', () => { + $globalDropdownMenu.removeClass('shortcuts'); + }); + + Mousetrap.bind('n', () => { + $globalDropdownMenu.toggleClass('shortcuts'); + $globalDropdownToggle.trigger('click'); + + if (!$globalDropdownMenu.is(':visible')) { + $globalDropdownToggle.blur(); + } + }); + + Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos')); + Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity')); + Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues')); + Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests')); + Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects')); + Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups')); + Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones')); + Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets')); + Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); if (typeof findFileURL !== "undefined" && findFileURL !== null) { Mousetrap.bind('t', function() { diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 4f1a19924a4..25f39e4fdb6 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,43 +1,12 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ -/* global Mousetrap */ -/* global Shortcuts */ - -require('./shortcuts'); - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.ShortcutsDashboardNavigation = (function(superClass) { - extend(ShortcutsDashboardNavigation, superClass); - - function ShortcutsDashboardNavigation() { - ShortcutsDashboardNavigation.__super__.constructor.call(this); - Mousetrap.bind('g a', function() { - return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity'); - }); - Mousetrap.bind('g i', function() { - return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues'); - }); - Mousetrap.bind('g m', function() { - return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests'); - }); - Mousetrap.bind('g t', function() { - return ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-todos'); - }); - Mousetrap.bind('g p', function() { - return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects'); - }); - } - - ShortcutsDashboardNavigation.findAndFollowLink = function(selector) { - var link; - link = $(selector).attr('href'); - if (link) { - return window.location = link; - } - }; - - return ShortcutsDashboardNavigation; - })(Shortcuts); -}).call(window); +/** + * Helper function that finds the href of the fiven selector and updates the location. + * + * @param {String} selector + */ +export default (selector) => { + const link = document.querySelector(selector).getAttribute('href'); + + if (link) { + window.location = link; + } +}; diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 3f5d6724417..c74ab0afd0c 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ /* global Mousetrap */ /* global Shortcuts */ +import findAndFollowLink from './shortcuts_dashboard_navigation'; require('./shortcuts'); @@ -13,59 +14,23 @@ require('./shortcuts'); function ShortcutsNavigation() { ShortcutsNavigation.__super__.constructor.call(this); - Mousetrap.bind('g p', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-project'); - }); - Mousetrap.bind('g e', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'); - }); - Mousetrap.bind('g f', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'); - }); - Mousetrap.bind('g c', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'); - }); - Mousetrap.bind('g b', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-builds'); - }); - Mousetrap.bind('g n', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-network'); - }); - Mousetrap.bind('g g', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-repository-charts'); - }); - Mousetrap.bind('g i', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'); - }); - Mousetrap.bind('g l', function() { - ShortcutsNavigation.findAndFollowLink('.shortcuts-issue-boards'); - }); - Mousetrap.bind('g m', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests'); - }); - Mousetrap.bind('g t', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-todos'); - }); - Mousetrap.bind('g w', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki'); - }); - Mousetrap.bind('g s', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets'); - }); - Mousetrap.bind('i', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue'); - }); + Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); + Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); + Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); + Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); + Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); + Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network')); + Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts')); + Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); + Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); + Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); + Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos')); + Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); + Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); + Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); this.enabledHelp.push('.hidden-shortcut.project'); } - ShortcutsNavigation.findAndFollowLink = function(selector) { - var link; - link = $(selector).attr('href'); - if (link) { - return window.location = link; - } - }; - return ShortcutsNavigation; })(Shortcuts); }).call(window); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js index 9bdc232b7da..5575aa72d5e 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import Visibility from 'visibilityjs'; import PipelinesService from './services/pipelines_service'; import eventHub from './event_hub'; import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; @@ -7,6 +8,7 @@ import EmptyState from './components/empty_state'; import ErrorState from './components/error_state'; import NavigationTabs from './components/navigation_tabs'; import NavigationControls from './components/nav_controls'; +import Poll from '../lib/utils/poll'; export default { props: { @@ -47,6 +49,7 @@ export default { pagenum: 1, isLoading: false, hasError: false, + isMakingRequest: false, }; }, @@ -120,18 +123,49 @@ export default { tagsPath: this.tagsPath, }; }, + + pageParameter() { + return gl.utils.getParameterByName('page') || this.pagenum; + }, + + scopeParameter() { + return gl.utils.getParameterByName('scope') || this.apiScope; + }, }, created() { this.service = new PipelinesService(this.endpoint); - this.fetchPipelines(); + const poll = new Poll({ + resource: this.service, + method: 'getPipelines', + data: { page: this.pageParameter, scope: this.scopeParameter }, + successCallback: this.successCallback, + errorCallback: this.errorCallback, + notificationCallback: this.setIsMakingRequest, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + poll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + poll.restart(); + } else { + poll.stop(); + } + }); eventHub.$on('refreshPipelines', this.fetchPipelines); }, beforeUpdate() { - if (this.state.pipelines.length && this.$children) { + if (this.state.pipelines.length && + this.$children && + !this.isMakingRequest && + !this.isLoading) { this.store.startTimeAgoLoops.call(this, Vue); } }, @@ -154,27 +188,35 @@ export default { }, fetchPipelines() { - const pageNumber = gl.utils.getParameterByName('page') || this.pagenum; - const scope = gl.utils.getParameterByName('scope') || this.apiScope; + if (!this.isMakingRequest) { + this.isLoading = true; - this.isLoading = true; - return this.service.getPipelines(scope, pageNumber) - .then(resp => ({ - headers: resp.headers, - body: resp.json(), - })) - .then((response) => { - this.store.storeCount(response.body.count); - this.store.storePipelines(response.body.pipelines); - this.store.storePagination(response.headers); - }) - .then(() => { - this.isLoading = false; - }) - .catch(() => { - this.hasError = true; - this.isLoading = false; - }); + this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter }) + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + }, + + successCallback(resp) { + const response = { + headers: resp.headers, + body: resp.json(), + }; + + this.store.storeCount(response.body.count); + this.store.storePipelines(response.body.pipelines); + this.store.storePagination(response.headers); + + this.isLoading = false; + }, + + errorCallback() { + this.hasError = true; + this.isLoading = false; + }, + + setIsMakingRequest(isMakingRequest) { + this.isMakingRequest = isMakingRequest; }, }, diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js index 708f5068dd3..255cd513490 100644 --- a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js +++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js @@ -26,7 +26,8 @@ export default class PipelinesService { this.pipelines = Vue.resource(endpoint); } - getPipelines(scope, page) { + getPipelines(data = {}) { + const { scope, page } = data; return this.pipelines.get({ scope, page }); } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 1ae144fb471..b849cc2d853 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -91,7 +91,7 @@ .award-menu-holder { display: inline-block; - position: relative; + position: absolute; .tooltip { white-space: nowrap; @@ -117,11 +117,41 @@ &.active, &:hover, - &:active { + &:active, + &.is-active { background-color: $row-hover; border-color: $row-hover-border; box-shadow: none; outline: 0; + + .award-control-icon svg { + background: $award-emoji-positive-add-bg; + + path { + fill: $award-emoji-positive-add-lines; + } + } + + .award-control-icon-neutral { + opacity: 0; + } + + .award-control-icon-positive { + opacity: 1; + transform: scale(1.15); + } + } + + &.is-active { + .award-control-icon-positive { + opacity: 0; + transform: scale(1); + } + + .award-control-icon-super-positive { + opacity: 1; + transform: scale(1); + } } &.btn { @@ -162,9 +192,33 @@ color: $border-gray-normal; margin-top: 1px; padding: 0 2px; + + svg { + margin-bottom: 1px; + height: 18px; + width: 18px; + border-radius: 50%; + + path { + fill: $border-gray-normal; + } + } + } + + .award-control-icon-positive, + .award-control-icon-super-positive { + position: absolute; + left: 7px; + bottom: 9px; + opacity: 0; + @include transition(opacity, transform); } .award-control-text { vertical-align: middle; } } + +.note-awards .award-control-icon-positive { + left: 6px; +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2ede47e9de6..7767826b033 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -177,10 +177,6 @@ border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; - .filtered-search-input-container & { - max-width: 280px; - } - &.is-loading { .dropdown-content { display: none; @@ -191,6 +187,15 @@ } } + .shortcut-mappings { + display: none; + } + + &.shortcuts .shortcut-mappings { + display: inline-block; + margin-right: 5px; + } + ul { margin: 0; padding: 0; @@ -467,6 +472,11 @@ overflow-y: auto; } +.dropdown-info-note { + color: $gl-text-color-secondary; + text-align: center; +} + .dropdown-footer { padding-top: 10px; margin-top: 10px; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ddea1cf540b..a5a8522739e 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -281,3 +281,16 @@ span.idiff { display: none; } } + +.file-fork-suggestion { + display: flex; + align-items: center; + justify-content: flex-end; + background-color: $gray-light; + border-bottom: 1px solid $border-color; + padding: 5px $gl-padding; +} + +.file-fork-suggestion-note { + margin-right: 1.5em; +} diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 5a034e11206..12465d4a70b 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -22,7 +22,6 @@ } @media (min-width: $screen-sm-min) { - .issues-filters, .issues_bulk_update { .dropdown-menu-toggle { width: 132px; @@ -56,7 +55,7 @@ } } -.filtered-search-container { +.filtered-search-wrapper { display: -webkit-flex; display: flex; @@ -151,11 +150,13 @@ width: 100%; } -.filtered-search-input-container { +.filtered-search-box { + position: relative; + flex: 1; display: -webkit-flex; display: flex; - position: relative; width: 100%; + min-width: 0; border: 1px solid $border-color; background-color: $white-light; @@ -163,14 +164,6 @@ -webkit-flex: 1 1 auto; flex: 1 1 auto; margin-bottom: 10px; - - .dropdown-menu { - width: auto; - left: 0; - right: 0; - max-width: none; - min-width: 100%; - } } &:hover { @@ -229,6 +222,118 @@ } } +.filtered-search-box-input-container { + flex: 1; + position: relative; + // Fix PhantomJS not supporting `flex: 1;` properly. + // This is important because it can change the expected `e.target` when clicking things in tests. + // See https://gitlab.com/gitlab-org/gitlab-ce/blob/b54acba8b732688c59fe2f38510c469dc86ee499/spec/features/issues/filtered_search/visual_tokens_spec.rb#L61 + // - With `width: 100%`: `e.target` = `.tokens-container`, https://i.imgur.com/jGq7wbx.png + // - Without `width: 100%`: `e.target` = `.filtered-search`, https://i.imgur.com/cNI2CyT.png + width: 100%; + min-width: 0; +} + +.filtered-search-input-dropdown-menu { + max-width: 280px; + + @media (max-width: $screen-xs-min) { + width: auto; + left: 0; + right: 0; + max-width: none; + min-width: 100%; + } +} + +.filtered-search-history-dropdown-toggle-button { + display: flex; + align-items: center; + width: auto; + height: 100%; + padding-top: 0; + padding-left: 0.75em; + padding-bottom: 0; + padding-right: 0.5em; + + background-color: transparent; + border-radius: 0; + border-top: 0; + border-left: 0; + border-bottom: 0; + border-right: 1px solid $border-color; + + color: $gl-text-color-secondary; + + transition: color 0.1s linear; + + &:hover, + &:focus { + color: $gl-text-color; + border-color: $dropdown-input-focus-border; + outline: none; + } + + .dropdown-toggle-text { + color: inherit; + + .fa { + color: inherit; + } + } + + .fa { + position: initial; + } + +} + +.filtered-search-history-dropdown-wrapper { + position: initial; + flex-shrink: 0; +} + +.filtered-search-history-dropdown { + width: 40%; + + @media (max-width: $screen-xs-min) { + left: 0; + right: 0; + max-width: none; + } +} + +.filtered-search-history-dropdown-content { + max-height: none; +} + +.filtered-search-history-dropdown-item, +.filtered-search-history-clear-button { + @include dropdown-link; + + overflow: hidden; + width: 100%; + margin: 0.5em 0; + + background-color: transparent; + border: 0; + text-align: left; + white-space: nowrap; + text-overflow: ellipsis; +} + +.filtered-search-history-dropdown-token { + display: inline; + + &:not(:last-child) { + margin-right: 0.3em; + } + + & > .value { + font-weight: 600; + } +} + .filter-dropdown-container { display: -webkit-flex; display: flex; @@ -248,10 +353,8 @@ } @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - .issues-details-filters { - .dropdown-menu-toggle { - width: 100px; - } + .issue-bulk-update-dropdown-toggle { + width: 100px; } } @@ -343,10 +446,8 @@ } } -.filter-dropdown-item.droplab-item-active { - .btn { - @extend %filter-dropdown-item-btn-hover; - } +.filter-dropdown-item.droplab-item-active .btn { + @extend %filter-dropdown-item-btn-hover; } .filter-dropdown-loading { diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 8cd49280e1c..7098203321d 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -16,6 +16,8 @@ body.modal-open { overflow: hidden; } -.modal .modal-dialog { - width: 860px; +@media (min-width: $screen-md-min) { + .modal-dialog { + width: 860px; + } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index ff185cd8767..cd23deb6d75 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -1,15 +1,18 @@ .timeline { @include basic-list; - margin: 0; padding: 0; .timeline-entry { - padding: $gl-padding $gl-btn-padding 11px; + padding: $gl-padding $gl-btn-padding 14px; border-color: $white-normal; color: $gl-text-color; border-bottom: 1px solid $border-white-light; + .timeline-entry-inner { + position: relative; + } + &:target { background: $line-target-blue; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 97794a47df8..712eb7caf33 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -293,6 +293,8 @@ $badge-color: $gl-text-color-secondary; * Award emoji */ $award-emoji-menu-shadow: rgba(0,0,0,.175); +$award-emoji-positive-add-bg: #fed159; +$award-emoji-positive-add-lines: #bb9c13; /* * Search Box diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 7c0fc1008d0..0be1c215959 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -197,7 +197,7 @@ .card { position: relative; - padding: 10px $gl-padding; + padding: 11px 10px 11px $gl-padding; background: $white-light; border-radius: $border-radius-default; box-shadow: 0 1px 2px $issue-boards-card-shadow; @@ -217,6 +217,8 @@ } .confidential-icon { + position: relative; + top: 1px; margin-right: 5px; } } @@ -224,34 +226,43 @@ .card-title { margin: 0; font-size: 1em; + line-height: inherit; a { - color: inherit; + color: $gl-text-color; word-wrap: break-word; + margin-right: 2px; } } -.card-footer { - margin-top: 5px; - line-height: 25px; - - .label { - margin-right: 5px; - font-size: (14px / $issue-boards-font-size) * 1em; - } +.card-header { + display: flex; + min-height: 20px; .card-assignee { + margin-left: auto; margin-right: 5px; + padding-left: 10px; + height: 20px; } .avatar { - margin-left: 0; - margin-right: 0; + margin: 0; + } +} + +.card-footer { + margin: 0 0 5px; + + .label { + margin-top: 5px; + margin-right: 6px; } } .card-number { - margin-right: 5px; + font-size: 12px; + color: $gl-text-color-secondary; } .issue-boards-search { diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss new file mode 100644 index 00000000000..3266714396e --- /dev/null +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -0,0 +1,16 @@ +/** + * Container Registry + */ + +.container-image { + border-bottom: 1px solid $white-normal; +} + +.container-image-head { + padding: 0 16px; + line-height: 4em; +} + +.table.tags { + margin-bottom: 0; +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 6faa3794c83..72e7d42858d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -233,6 +233,15 @@ stroke-width: 1; } +.prometheus-state { + margin-top: 10px; + display: none; + + .state-button-section { + margin-top: 10px; + } +} + .environments-actions { .external-url, .monitoring-url, diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 08398bb43a2..e7f9bbbc62f 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,14 +4,14 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); + padding: $gl-padding-top 0 $gl-padding-top 40px; border-bottom: 1px solid $white-normal; color: $list-text-color; + position: relative; &.event-inline { - .avatar { - position: relative; - top: -2px; + .profile-icon { + top: 20px; } .event-title, @@ -24,8 +24,28 @@ color: $gl-text-color; } - .avatar { - margin-left: -($gl-avatar-size + $gl-padding-top); + .profile-icon { + position: absolute; + left: 0; + top: 14px; + + svg { + width: 20px; + height: auto; + fill: $gl-text-color-secondary; + } + + &.open-icon svg { + fill: $green-300; + } + + &.closed-icon svg { + fill: $red-300; + } + + &.fork-icon svg { + fill: $blue-300; + } } .event-title { @@ -163,7 +183,7 @@ max-width: 100%; } - .avatar { + .profile-icon { display: none; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 566dcc64802..2f946ab2f59 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -329,8 +329,6 @@ } #modal_merge_info .modal-dialog { - width: 600px; - .dark { margin-right: 40px; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 57cf8e136e2..12ca20a1420 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -16,6 +16,15 @@ ul.notes { .timeline-icon { float: left; + + svg { + width: 18px; + height: auto; + fill: $gray-darkest; + position: absolute; + left: 30px; + top: 15px; + } } .timeline-content { @@ -33,6 +42,103 @@ ul.notes { white-space: nowrap; } + .discussion-body { + padding-top: 15px; + } + + .discussion { + overflow: hidden; + display: block; + position: relative; + } + + .note { + display: block; + position: relative; + border-bottom: 1px solid $white-normal; + + &.note-discussion { + &.timeline-entry { + padding: 14px 10px; + } + + .system-note { + padding: 0; + } + } + + &.is-editting { + .note-header, + .note-text, + .edited-text { + display: none; + } + + .note-edit-form { + display: block; + + &.current-note-edit-form + .note-awards { + display: none; + } + } + } + + .note-body { + overflow-x: auto; + overflow-y: hidden; + + .note-text { + word-wrap: break-word; + @include md-typography; + // Reset ul style types since we're nested inside a ul already + @include bulleted-list; + ul.task-list { + ul:not(.task-list) { + padding-left: 1.3em; + } + } + } + } + + .note-awards { + .js-awards-block { + padding: 2px; + margin-top: 10px; + } + } + + .note-header { + padding-bottom: 3px; + padding-right: 20px; + + @media (min-width: $screen-sm-min) { + padding-right: 0; + } + + @media (max-width: $screen-xs-min) { + .inline { + display: block; + } + } + } + + .note-emoji-button { + .fa-spinner { + display: none; + } + + &.is-loading { + .fa-smile-o { + display: none; + } + + .fa-spinner { + display: inline-block; + } + } + } + } + .system-note { font-size: 14px; padding: 0; @@ -68,6 +174,10 @@ ul.notes { padding: 14px 10px; } + .note-header { + padding-bottom: 0; + } + .note-body { overflow: hidden; @@ -130,116 +240,6 @@ ul.notes { } } } - - .timeline-icon { - display: none; - - .avatar { - visibility: hidden; - - .discussion-body & { - visibility: visible; - } - } - } - } - - .discussion-body { - padding-top: 15px; - } - - .discussion { - overflow: hidden; - display: block; - position: relative; - } - - .note { - display: block; - position: relative; - border-bottom: 1px solid $white-normal; - - &.note-discussion { - &.timeline-entry { - padding: 14px 10px; - } - - .system-note { - padding: 0; - } - } - - &.is-editting { - .note-header, - .note-text, - .edited-text { - display: none; - } - - .note-edit-form { - display: block; - - &.current-note-edit-form + .note-awards { - display: none; - } - } - } - - .note-body { - overflow-x: auto; - overflow-y: hidden; - - .note-text { - word-wrap: break-word; - @include md-typography; - // Reset ul style types since we're nested inside a ul already - @include bulleted-list; - ul.task-list { - ul:not(.task-list) { - padding-left: 1.3em; - } - } - } - } - - .note-awards { - .js-awards-block { - padding: 2px; - margin-top: 10px; - } - } - - .note-header { - padding-bottom: 3px; - padding-right: 20px; - - @media (min-width: $screen-sm-min) { - padding-right: 0; - } - - @media (max-width: $screen-xs-min) { - .inline { - display: block; - } - } - } - - .note-emoji-button { - .fa-spinner { - display: none; - } - - &.is-loading { - .fa-smile-o { - display: none; - } - - .fa-spinner { - display: inline-block; - } - } - } - } } @@ -398,13 +398,50 @@ ul.notes { font-size: 17px; } - &:hover { + svg { + height: 16px; + width: 16px; + fill: $gray-darkest; + vertical-align: text-top; + } + + .award-control-icon-positive, + .award-control-icon-super-positive { + position: absolute; + margin-left: -20px; + opacity: 0; + } + + &:hover, + &.is-active { .danger-highlight { color: $gl-text-red; } .link-highlight { color: $gl-link-color; + + svg { + fill: $gl-link-color; + } + } + + .award-control-icon-neutral { + opacity: 0; + } + + .award-control-icon-positive { + opacity: 1; + } + } + + &.is-active { + .award-control-icon-positive { + opacity: 0; + } + + .award-control-icon-super-positive { + opacity: 1; } } } @@ -508,7 +545,6 @@ ul.notes { } .line-resolve-all-container { - .btn-group { margin-left: -4px; } @@ -537,7 +573,6 @@ ul.notes { fill: $gray-darkest; } } - } .line-resolve-all { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 703c5fc8869..8c6dd392865 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -230,6 +230,14 @@ font-size: 0; } + .fade-right { + right: 0; + } + + .fade-left { + left: 0; + } + @media (max-width: $screen-xs-max) { .cover-block { padding-top: 20px; diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss index b97a29cd1a0..fe22d186af1 100644 --- a/app/assets/stylesheets/pages/settings_ci_cd.scss +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -6,6 +6,8 @@ } .trigger-actions { + white-space: nowrap; + .btn { margin-left: 10px; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index fc4da4c495f..f3916622b6f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -145,8 +145,6 @@ margin: 0; } -#modal-remove-blob > .modal-dialog { width: 850px; } - .blob-upload-dropzone-previews { text-align: center; border: 2px; diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index cea3d088e94..f28bbdeff5a 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -72,7 +72,9 @@ class Admin::GroupsController < Admin::ApplicationController :name, :path, :request_access_enabled, - :visibility_level + :visibility_level, + :require_two_factor_authentication, + :two_factor_grace_period ] end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6a6e335d314..e77094fe2a8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -8,12 +8,12 @@ class ApplicationController < ActionController::Base include PageLayoutHelper include SentryHelper include WorkhorseHelper + include EnforcesTwoFactorAuthentication before_action :authenticate_user_from_private_token! before_action :authenticate_user! before_action :validate_user_service_ticket! before_action :check_password_expiration - before_action :check_2fa_requirement before_action :ldap_security_check before_action :sentry_context before_action :default_headers @@ -151,12 +151,6 @@ class ApplicationController < ActionController::Base end end - def check_2fa_requirement - if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor? - redirect_to profile_two_factor_auth_path - end - end - def ldap_security_check if current_user && current_user.requires_ldap_check? return unless current_user.try_obtain_ldap_lease @@ -265,23 +259,6 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('gitlab_project') end - def two_factor_authentication_required? - current_application_settings.require_two_factor_authentication - end - - def two_factor_grace_period - current_application_settings.two_factor_grace_period - end - - def two_factor_grace_period_expired? - date = current_user.otp_grace_period_started_at - date && (date + two_factor_grace_period.hours) < Time.current - end - - def skip_two_factor? - session[:skip_tfa] && session[:skip_tfa] > Time.current - end - # U2F (universal 2nd factor) devices need a unique identifier for the application # to perform authentication. # https://developers.yubico.com/U2F/App_ID.html diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb new file mode 100644 index 00000000000..688e8bd4a37 --- /dev/null +++ b/app/controllers/concerns/enforces_two_factor_authentication.rb @@ -0,0 +1,58 @@ +# == EnforcesTwoFactorAuthentication +# +# Controller concern to enforce two-factor authentication requirements +# +# Upon inclusion, adds `check_two_factor_requirement` as a before_action, +# and makes `two_factor_grace_period_expired?` and `two_factor_skippable?` +# available as view helpers. +module EnforcesTwoFactorAuthentication + extend ActiveSupport::Concern + + included do + before_action :check_two_factor_requirement + helper_method :two_factor_grace_period_expired?, :two_factor_skippable? + end + + def check_two_factor_requirement + if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor? + redirect_to profile_two_factor_auth_path + end + end + + def two_factor_authentication_required? + current_application_settings.require_two_factor_authentication? || + current_user.try(:require_two_factor_authentication_from_group?) + end + + def two_factor_authentication_reason(global: -> {}, group: -> {}) + if two_factor_authentication_required? + if current_application_settings.require_two_factor_authentication? + global.call + else + groups = current_user.expanded_groups_requiring_two_factor_authentication.reorder(name: :asc) + group.call(groups) + end + end + end + + def two_factor_grace_period + periods = [current_application_settings.two_factor_grace_period] + periods << current_user.two_factor_grace_period if current_user.try(:require_two_factor_authentication_from_group?) + periods.min + end + + def two_factor_grace_period_expired? + date = current_user.otp_grace_period_started_at + date && (date + two_factor_grace_period.hours) < Time.current + end + + def two_factor_skippable? + two_factor_authentication_required? && + !current_user.two_factor_enabled? && + !two_factor_grace_period_expired? + end + + def skip_two_factor? + session[:skip_two_factor] && session[:skip_two_factor] > Time.current + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 78c9f1f7004..593001e6396 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -151,7 +151,9 @@ class GroupsController < Groups::ApplicationController :visibility_level, :parent_id, :create_chat_team, - :chat_team_name + :chat_team_name, + :require_two_factor_authentication, + :two_factor_grace_period ] end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 26e7e93533e..d3fa81cd623 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -1,5 +1,5 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController - skip_before_action :check_2fa_requirement + skip_before_action :check_two_factor_requirement def show unless current_user.otp_secret @@ -13,11 +13,24 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController current_user.save! if current_user.changed? if two_factor_authentication_required? && !current_user.two_factor_enabled? - if two_factor_grace_period_expired? - flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.' - else + two_factor_authentication_reason( + global: lambda do + flash.now[:alert] = + 'The global settings require you to enable Two-Factor Authentication for your account.' + end, + group: lambda do |groups| + group_links = groups.map { |group| view_context.link_to group.full_name, group_path(group) }.to_sentence + + flash.now[:alert] = %{ + The group settings for #{group_links} require you to enable + Two-Factor Authentication for your account. + }.html_safe + end + ) + + unless two_factor_grace_period_expired? grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}." + flash.now[:alert] << " You need to do this before #{l(grace_period_deadline)}." end end @@ -71,7 +84,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController if two_factor_grace_period_expired? redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup' else - session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours + session[:skip_two_factor] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours redirect_to root_path end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 80a95c6158b..73706bf8dae 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -7,9 +7,11 @@ class Projects::BlobController < Projects::ApplicationController # Raised when given an invalid file path InvalidPathError = Class.new(StandardError) + prepend_before_action :authenticate_user!, only: [:edit] + before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! - before_action :authorize_edit_tree!, only: [:new, :create, :edit, :update, :destroy] + before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy] before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] @@ -37,7 +39,11 @@ class Projects::BlobController < Projects::ApplicationController end def edit - blob.load_all_data!(@repository) + if can_collaborate_with_project? + blob.load_all_data!(@repository) + else + redirect_to action: 'show' + end end def update diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 3f3c90a49ab..add66ce9f84 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -31,25 +31,25 @@ class Projects::BuildsController < Projects::ApplicationController @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC') @builds = @builds.where("id not in (?)", @build.id) @pipeline = @build.pipeline - - respond_to do |format| - format.html - format.json do - render json: { - id: @build.id, - status: @build.status, - trace_html: @build.trace_html - } - end - end end def trace - respond_to do |format| - format.json do - state = params[:state].presence - render json: @build.trace_with_state(state: state). - merge!(id: @build.id, status: @build.status) + build.trace.read do |stream| + respond_to do |format| + format.json do + result = { + id: @build.id, status: @build.status, complete: @build.complete? + } + + if stream.valid? + stream.limit + state = params[:state].presence + trace = stream.html_with_state(state) + result.merge!(trace.to_h) + end + + render json: result + end end end end @@ -86,10 +86,12 @@ class Projects::BuildsController < Projects::ApplicationController end def raw - if @build.has_trace_file? - send_file @build.trace_file_path, type: 'text/plain; charset=utf-8', disposition: 'inline' - else - render_404 + build.trace.read do |stream| + if stream.file? + send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline' + else + render_404 + end end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index cc67f688d51..f453822bed6 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -35,6 +35,8 @@ class Projects::CommitController < Projects::ApplicationController respond_to do |format| format.html format.json do + Gitlab::PollingInterval.set_header(response, interval: 10_000) + render json: PipelineSerializer .new(project: @project, user: @current_user) .represent(@pipelines) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb deleted file mode 100644 index d1f46497207..00000000000 --- a/app/controllers/projects/container_registry_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -class Projects::ContainerRegistryController < Projects::ApplicationController - before_action :verify_registry_enabled - before_action :authorize_read_container_image! - before_action :authorize_update_container_image!, only: [:destroy] - layout 'project' - - def index - @tags = container_registry_repository.tags - end - - def destroy - url = namespace_project_container_registry_index_path(project.namespace, project) - - if tag.delete - redirect_to url - else - redirect_to url, alert: 'Failed to remove tag' - end - end - - private - - def verify_registry_enabled - render_404 unless Gitlab.config.registry.enabled - end - - def container_registry_repository - @container_registry_repository ||= project.container_registry_repository - end - - def tag - @tag ||= container_registry_repository.tag(params[:id]) - end -end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a79d801991a..c107b3ffa88 100755 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -233,6 +233,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end format.json do + Gitlab::PollingInterval.set_header(response, interval: 10_000) + render json: PipelineSerializer .new(project: @project, user: @current_user) .represent(@pipelines) @@ -246,6 +248,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController format.json do define_pipelines_vars + Gitlab::PollingInterval.set_header(response, interval: 10_000) + render json: { pipelines: PipelineSerializer .new(project: @project, user: @current_user) @@ -452,7 +456,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController if pipeline status = pipeline.status - coverage = pipeline.try(:coverage) + coverage = pipeline.coverage status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings? diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 43a1abaa662..1780cc0233c 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController respond_to do |format| format.html format.json do + Gitlab::PollingInterval.set_header(response, interval: 10_000) + render json: { pipelines: PipelineSerializer .new(project: @project, user: @current_user) @@ -114,7 +116,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def pipeline - @pipeline ||= project.pipelines.find_by!(id: params[:id]) + @pipeline ||= project.pipelines.find_by!(id: params[:id]).present(current_user: current_user) end def commit diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index c8c80551ac9..ff50602831c 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController def update_params params.require(:project).permit( :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds + :public_builds, :auto_cancel_pending_pipelines ) end end diff --git a/app/controllers/projects/registry/application_controller.rb b/app/controllers/projects/registry/application_controller.rb new file mode 100644 index 00000000000..a56f9c58726 --- /dev/null +++ b/app/controllers/projects/registry/application_controller.rb @@ -0,0 +1,16 @@ +module Projects + module Registry + class ApplicationController < Projects::ApplicationController + layout 'project' + + before_action :verify_registry_enabled! + before_action :authorize_read_container_image! + + private + + def verify_registry_enabled! + render_404 unless Gitlab.config.registry.enabled + end + end + end +end diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb new file mode 100644 index 00000000000..17f391ba07f --- /dev/null +++ b/app/controllers/projects/registry/repositories_controller.rb @@ -0,0 +1,43 @@ +module Projects + module Registry + class RepositoriesController < ::Projects::Registry::ApplicationController + before_action :authorize_update_container_image!, only: [:destroy] + before_action :ensure_root_container_repository!, only: [:index] + + def index + @images = project.container_repositories + end + + def destroy + if image.destroy + redirect_to project_container_registry_path(@project), + notice: 'Image repository has been removed successfully!' + else + redirect_to project_container_registry_path(@project), + alert: 'Failed to remove image repository!' + end + end + + private + + def image + @image ||= project.container_repositories.find(params[:id]) + end + + ## + # Container repository object for root project path. + # + # Needed to maintain a backwards compatibility. + # + def ensure_root_container_repository! + ContainerRegistry::Path.new(@project.full_path).tap do |path| + break if path.has_repository? + + ContainerRepository.build_from_path(path).tap do |repository| + repository.save! if repository.has_tags? + end + end + end + end + end +end diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb new file mode 100644 index 00000000000..d689cade3ab --- /dev/null +++ b/app/controllers/projects/registry/tags_controller.rb @@ -0,0 +1,28 @@ +module Projects + module Registry + class TagsController < ::Projects::Registry::ApplicationController + before_action :authorize_update_container_image!, only: [:destroy] + + def destroy + if tag.delete + redirect_to project_container_registry_path(@project), + notice: 'Registry tag has been removed successfully!' + else + redirect_to project_container_registry_path(@project), + alert: 'Failed to remove registry tag!' + end + end + + private + + def image + @image ||= project.container_repositories + .find(params[:repository_id]) + end + + def tag + @tag ||= image.tag(params[:id]) + end + end + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d8561871098..d3091a4f8e9 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -3,7 +3,7 @@ class SessionsController < Devise::SessionsController include Devise::Controllers::Rememberable include Recaptcha::ClientHelper - skip_before_action :check_2fa_requirement, only: [:destroy] + skip_before_action :check_two_factor_requirement, only: [:destroy] prepend_before_action :check_initial_setup, only: [:new] prepend_before_action :authenticate_with_two_factor, diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 101fe579da2..9c71d6c7f4c 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -64,18 +64,6 @@ module AuthHelper current_user.identities.exists?(provider: provider.to_s) end - def two_factor_skippable? - current_application_settings.require_two_factor_authentication && - !current_user.two_factor_enabled? && - current_application_settings.two_factor_grace_period && - !two_factor_grace_period_expired? - end - - def two_factor_grace_period_expired? - current_user.otp_grace_period_started_at && - (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current - end - def unlink_allowed?(provider) %w(saml cas3).exclude?(provider.to_s) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 8631bc54509..b0ac1623fbe 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -8,31 +8,36 @@ module BlobHelper %w(credits changelog news copying copyright license authors) end - def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) - return unless current_user + def edit_path(project = @project, ref = @ref, path = @path, options = {}) + namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + options[:link_opts]) + end + def fork_path(project = @project, ref = @ref, path = @path, options = {}) + continue_params = { + to: edit_path, + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now + } + namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) + 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 - edit_path = namespace_project_edit_blob_path(project.namespace, project, - tree_join(ref, path), - options[:link_opts]) + common_classes = "btn js-edit-blob #{options[:extra_class]}" if !on_top_of_branch?(project, ref) - button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } - elsif can_edit_blob?(blob, project, ref) - link_to "Edit", edit_path, class: 'btn btn-sm' - elsif can?(current_user, :fork_project, project) - continue_params = { - to: edit_path, - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - - link_to "Edit", fork_path, class: 'btn', method: :post + 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_edit_blob?(blob, project, ref)) + link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" + elsif current_user && can?(current_user, :fork_project, project) + button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler" end end @@ -97,7 +102,7 @@ module BlobHelper if Gitlab::MarkupHelper.previewable?(filename) 'Preview' else - 'Preview Changes' + 'Preview changes' end end @@ -205,13 +210,13 @@ module BlobHelper end def copy_file_path_button(file_path) - clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') + clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') end def copy_blob_content_button(blob) return if markup?(blob.name) - clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard") + clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard") end def open_raw_file_button(path) diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 0b30471f2ae..c85e96cf78d 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -1,23 +1,42 @@ module ButtonHelper # Output a "Copy to Clipboard" button # - # data - Data attributes passed to `content_tag` + # data - Data attributes passed to `content_tag` (default: {}): + # :text - Text to copy (optional) + # :gfm - GitLab Flavored Markdown to copy, if different from `text` (optional) + # :target - Selector for target element to copy from (optional) # # Examples: # # # Define the clipboard's text - # clipboard_button(clipboard_text: "Foo") + # clipboard_button(text: "Foo") # # => "<button class='...' data-clipboard-text='Foo'>...</button>" # # # Define the target element - # clipboard_button(clipboard_target: "div#foo") + # clipboard_button(target: "div#foo") # # => "<button class='...' data-clipboard-target='div#foo'>...</button>" # # See http://clipboardjs.com/#usage def clipboard_button(data = {}) css_class = data[:class] || 'btn-clipboard btn-transparent' title = data[:title] || 'Copy to clipboard' + + # This supports code in app/assets/javascripts/copy_to_clipboard.js that + # works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM. + if text = data.delete(:text) + data[:clipboard_text] = + if gfm = data.delete(:gfm) + { text: text, gfm: gfm } + else + text + end + end + + target = data.delete(:target) + data[:clipboard_target] = target if target + data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) + content_tag :button, icon('clipboard', 'aria-hidden': 'true'), class: "btn #{css_class}", diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 81e0b6bb5ae..8ed99642c7a 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -1,6 +1,6 @@ module DropdownsHelper def dropdown_tag(toggle_text, options: {}, &block) - content_tag :div, class: "dropdown" do + content_tag :div, class: "dropdown #{options[:wrapper_class] if options.has_key?(:wrapper_class)}" do data_attr = { toggle: "dropdown" } if options.has_key?(:data) @@ -20,7 +20,7 @@ module DropdownsHelper output << dropdown_filter(options[:placeholder]) end - output << content_tag(:div, class: "dropdown-content") do + output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.has_key?(:content_class)}") do capture(&block) if block && !options.has_key?(:footer_content) end diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb new file mode 100644 index 00000000000..3074921caff --- /dev/null +++ b/app/helpers/system_note_helper.rb @@ -0,0 +1,26 @@ +module SystemNoteHelper + ICON_NAMES_BY_ACTION = { + 'commit' => 'icon_commit', + 'merge' => 'icon_merge', + 'merged' => 'icon_merged', + 'opened' => 'icon_status_open', + 'closed' => 'icon_status_closed', + 'time_tracking' => 'icon_stopwatch', + 'assignee' => 'icon_user', + 'title' => 'icon_pencil', + 'task' => 'icon_check_square_o', + 'label' => 'icon_tags', + 'cross_reference' => 'icon_random', + 'branch' => 'icon_code_fork', + 'confidential' => 'icon_eye_slash', + 'visible' => 'icon_eye', + 'milestone' => 'icon_clock_o', + 'discussion' => 'icon_comment_o', + 'moved' => 'icon_arrow_circle_o_right' + }.freeze + + def icon_for_system_note(note) + icon_name = ICON_NAMES_BY_ACTION[note.system_note_metadata&.action] + custom_icon(icon_name) if icon_name + end +end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 6937ad3bdd9..6ada6fae4eb 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -3,13 +3,14 @@ class AwardEmoji < ActiveRecord::Base UPVOTE_NAME = "thumbsup".freeze include Participable + include GhostUser belongs_to :awardable, polymorphic: true belongs_to :user validates :awardable, :user, presence: true validates :name, presence: true, inclusion: { in: Gitlab::Emoji.emojis_names } - validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] } + validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }, unless: :ghost_user? participant :user diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8431c5f228c..b426c27afbb 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -103,18 +103,13 @@ module Ci end def playable? - project.builds_enabled? && has_commands? && - action? && manual? + action? && manual? end def action? self.when == 'manual' end - def has_commands? - commands.present? - end - def play(current_user) # Try to queue a current build if self.enqueue @@ -131,8 +126,7 @@ module Ci end def retryable? - project.builds_enabled? && has_commands? && - (success? || failed? || canceled?) + success? || failed? || canceled? end def retried? @@ -171,19 +165,6 @@ module Ci latest_builds.where('stage_idx < ?', stage_idx) end - def trace_html(**args) - trace_with_state(**args)[:html] || '' - end - - def trace_with_state(state: nil, last_lines: nil) - trace_ansi = trace(last_lines: last_lines) - if trace_ansi.present? - Ci::Ansi2html.convert(trace_ansi, state) - else - {} - end - end - def timeout project.build_timeout end @@ -244,136 +225,35 @@ module Ci end def update_coverage - coverage = extract_coverage(trace, coverage_regex) + coverage = trace.extract_coverage(coverage_regex) update_attributes(coverage: coverage) if coverage.present? end - def extract_coverage(text, regex) - return unless regex - - matches = text.scan(Regexp.new(regex)).last - matches = matches.last if matches.is_a?(Array) - coverage = matches.gsub(/\d+(\.\d+)?/).first - - if coverage.present? - coverage.to_f - end - rescue - # if bad regex or something goes wrong we dont want to interrupt transition - # so we just silentrly ignore error for now - end - - def has_trace_file? - File.exist?(path_to_trace) || has_old_trace_file? + def trace + Gitlab::Ci::Trace.new(self) end def has_trace? - raw_trace.present? - end - - def raw_trace(last_lines: nil) - if File.exist?(trace_file_path) - Gitlab::Ci::TraceReader.new(trace_file_path). - read(last_lines: last_lines) - else - # backward compatibility - read_attribute :trace - end - end - - ## - # Deprecated - # - # This is a hotfix for CI build data integrity, see #4246 - def has_old_trace_file? - project.ci_id && File.exist?(old_path_to_trace) - end - - def trace(last_lines: nil) - hide_secrets(raw_trace(last_lines: last_lines)) + trace.exist? end - def trace_length - if raw_trace - raw_trace.bytesize - else - 0 - end + def trace=(data) + raise NotImplementedError end - def trace=(trace) - recreate_trace_dir - trace = hide_secrets(trace) - File.write(path_to_trace, trace) - end - - def recreate_trace_dir - unless Dir.exist?(dir_to_trace) - FileUtils.mkdir_p(dir_to_trace) - end + def old_trace + read_attribute(:trace) end - private :recreate_trace_dir - def append_trace(trace_part, offset) - recreate_trace_dir - touch if needs_touch? - - trace_part = hide_secrets(trace_part) - - File.truncate(path_to_trace, offset) if File.exist?(path_to_trace) - File.open(path_to_trace, 'ab') do |f| - f.write(trace_part) - end + def erase_old_trace! + write_attribute(:trace, nil) + save end def needs_touch? Time.now - updated_at > 15.minutes.to_i end - def trace_file_path - if has_old_trace_file? - old_path_to_trace - else - path_to_trace - end - end - - def dir_to_trace - File.join( - Settings.gitlab_ci.builds_path, - created_at.utc.strftime("%Y_%m"), - project.id.to_s - ) - end - - def path_to_trace - "#{dir_to_trace}/#{id}.log" - end - - ## - # Deprecated - # - # This is a hotfix for CI build data integrity, see #4246 - # Should be removed in 8.4, after CI files migration has been done. - # - def old_dir_to_trace - File.join( - Settings.gitlab_ci.builds_path, - created_at.utc.strftime("%Y_%m"), - project.ci_id.to_s - ) - end - - ## - # Deprecated - # - # This is a hotfix for CI build data integrity, see #4246 - # Should be removed in 8.4, after CI files migration has been done. - # - def old_path_to_trace - "#{old_dir_to_trace}/#{id}.log" - end - ## # Deprecated # @@ -555,6 +435,15 @@ module Ci options[:dependencies]&.empty? end + def hide_secrets(trace) + return unless trace + + trace = trace.dup + Ci::MaskSecret.mask!(trace, project.runners_token) if project + Ci::MaskSecret.mask!(trace, token) + trace + end + private def update_artifacts_size @@ -566,7 +455,7 @@ module Ci end def erase_trace! - self.trace = nil + trace.erase! end def update_erased!(user = nil) @@ -628,15 +517,6 @@ module Ci pipeline.config_processor.build_attributes(name) end - def hide_secrets(trace) - return unless trace - - trace = trace.dup - Ci::MaskSecret.mask!(trace, project.runners_token) if project - Ci::MaskSecret.mask!(trace, token) - trace - end - def update_project_statistics return unless project diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 49dec770096..37a81fa7781 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -4,14 +4,25 @@ module Ci include HasStatus include Importable include AfterCommitQueue + include Presentable belongs_to :project belongs_to :user + belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' + + has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' + has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :builds, foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id + has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build' + has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build' + has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus' + has_many :manual_actions, -> { latest.manual_actions }, foreign_key: :commit_id, class_name: 'Ci::Build' + has_many :artifacts, -> { latest.with_artifacts_not_expired }, foreign_key: :commit_id, class_name: 'Ci::Build' + delegate :id, to: :project, prefix: true validates :sha, presence: { unless: :importing? } @@ -65,6 +76,10 @@ module Ci pipeline.update_duration end + before_transition canceled: any - [:canceled] do |pipeline| + pipeline.auto_canceled_by = nil + end + after_transition [:created, :pending] => :running do |pipeline| pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } end @@ -82,6 +97,8 @@ module Ci pipeline.run_after_commit do PipelineHooksWorker.perform_async(id) + Ci::ExpirePipelineCacheService.new(project, nil) + .execute(pipeline) end end @@ -160,10 +177,6 @@ module Ci end end - def artifacts - builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) - end - def valid_commit_sha if self.sha == Gitlab::Git::BLANK_SHA self.errors.add(:sha, " cant be 00000000 (branch removal)") @@ -200,27 +213,37 @@ module Ci !tag? end - def manual_actions - builds.latest.manual_actions.includes(project: [:namespace]) - end - def stuck? - builds.pending.includes(:project).any?(&:stuck?) + pending_builds.any?(&:stuck?) end def retryable? - builds.latest.failed_or_canceled.any?(&:retryable?) + retryable_builds.any? end def cancelable? - statuses.cancelable.any? + cancelable_statuses.any? + end + + def auto_canceled? + canceled? && auto_canceled_by_id? end def cancel_running - Gitlab::OptimisticLocking.retry_lock( - statuses.cancelable) do |cancelable| - cancelable.find_each(&:cancel) + Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable| + cancelable.find_each do |job| + yield(job) if block_given? + job.cancel end + end + end + + def auto_cancel_running(pipeline) + update(auto_canceled_by: pipeline) + + cancel_running do |job| + job.auto_canceled_by = pipeline + end end def retry_failed(current_user) diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index cba1d81a861..0a89f3e0640 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -8,6 +8,7 @@ module Ci belongs_to :owner, class_name: "User" has_many :trigger_requests, dependent: :destroy + has_one :trigger_schedule, dependent: :destroy validates :token, presence: true, uniqueness: true diff --git a/app/models/ci/trigger_schedule.rb b/app/models/ci/trigger_schedule.rb new file mode 100644 index 00000000000..10ea381ee31 --- /dev/null +++ b/app/models/ci/trigger_schedule.rb @@ -0,0 +1,30 @@ +module Ci + class TriggerSchedule < ActiveRecord::Base + extend Ci::Model + include Importable + + acts_as_paranoid + + belongs_to :project + belongs_to :trigger + + delegate :ref, to: :trigger + + validates :trigger, presence: { unless: :importing? } + validates :cron, cron: true, presence: { unless: :importing? } + validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } + validates :ref, presence: { unless: :importing? } + + before_save :set_next_run_at + + def set_next_run_at + self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) + end + + def schedule_next_run! + save! # with set_next_run_at + rescue ActiveRecord::RecordInvalid + update_attribute(:next_run_at, nil) # update without validation + end + end +end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 17b322b5ae3..2c4033146bf 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -7,6 +7,7 @@ class CommitStatus < ActiveRecord::Base belongs_to :project belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id + belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :user delegate :commit, to: :pipeline @@ -137,6 +138,10 @@ class CommitStatus < ActiveRecord::Base false end + def auto_canceled? + canceled? && auto_canceled_by_id? + end + # Added in 9.0 to keep backward compatibility for projects exported in 8.17 # and prior. def gl_project_id diff --git a/app/models/concerns/ghost_user.rb b/app/models/concerns/ghost_user.rb new file mode 100644 index 00000000000..da696127a80 --- /dev/null +++ b/app/models/concerns/ghost_user.rb @@ -0,0 +1,7 @@ +module GhostUser + extend ActiveSupport::Concern + + def ghost_user? + user && user.ghost? + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 0a1a65da05a..2f61709110c 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -76,6 +76,7 @@ module HasStatus scope :canceled, -> { where(status: 'canceled') } scope :skipped, -> { where(status: 'skipped') } scope :manual, -> { where(status: 'manual') } + scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 529fb5ce988..aca99feee53 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -83,6 +83,74 @@ module Routable AND members.source_type = r2.source_type"). where('members.user_id = ?', user_id) end + + # Builds a relation to find multiple objects that are nested under user + # membership. Includes the parent, as opposed to `#member_descendants` + # which only includes the descendants. + # + # Usage: + # + # Klass.member_self_and_descendants(1) + # + # Returns an ActiveRecord::Relation. + def member_self_and_descendants(user_id) + joins(:route). + joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%') + OR routes.path = r2.path + INNER JOIN members ON members.source_id = r2.source_id + AND members.source_type = r2.source_type"). + where('members.user_id = ?', user_id) + end + + # Returns all objects in a hierarchy, where any node in the hierarchy is + # under the user membership. + # + # Usage: + # + # Klass.member_hierarchy(1) + # + # Examples: + # + # Given the following group tree... + # + # _______group_1_______ + # | | + # | | + # nested_group_1 nested_group_2 + # | | + # | | + # nested_group_1_1 nested_group_2_1 + # + # + # ... the following results are returned: + # + # * the user is a member of group 1 + # => 'group_1', + # 'nested_group_1', nested_group_1_1', + # 'nested_group_2', 'nested_group_2_1' + # + # * the user is a member of nested_group_2 + # => 'group1', + # 'nested_group_2', 'nested_group_2_1' + # + # * the user is a member of nested_group_2_1 + # => 'group1', + # 'nested_group_2', 'nested_group_2_1' + # + # Returns an ActiveRecord::Relation. + def member_hierarchy(user_id) + paths = member_self_and_descendants(user_id).pluck('routes.path') + + return none if paths.empty? + + wheres = paths.map do |path| + "#{connection.quote(path)} = routes.path + OR + #{connection.quote(path)} LIKE CONCAT(routes.path, '/%')" + end + + joins(:route).where(wheres.join(' OR ')) + end end def full_name diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb new file mode 100644 index 00000000000..9682df3a586 --- /dev/null +++ b/app/models/container_repository.rb @@ -0,0 +1,77 @@ +class ContainerRepository < ActiveRecord::Base + belongs_to :project + + validates :name, length: { minimum: 0, allow_nil: false } + validates :name, uniqueness: { scope: :project_id } + + delegate :client, to: :registry + + before_destroy :delete_tags! + + def registry + @registry ||= begin + token = Auth::ContainerRegistryAuthenticationService.full_access_token(path) + + url = Gitlab.config.registry.api_url + host_port = Gitlab.config.registry.host_port + + ContainerRegistry::Registry.new(url, token: token, path: host_port) + end + end + + def path + @path ||= [project.full_path, name].select(&:present?).join('/') + end + + def tag(tag) + ContainerRegistry::Tag.new(self, tag) + end + + def manifest + @manifest ||= client.repository_tags(path) + end + + def tags + return @tags if defined?(@tags) + return [] unless manifest && manifest['tags'] + + @tags = manifest['tags'].map do |tag| + ContainerRegistry::Tag.new(self, tag) + end + end + + def blob(config) + ContainerRegistry::Blob.new(self, config) + end + + def has_tags? + tags.any? + end + + def root_repository? + name.empty? + end + + def delete_tags! + return unless has_tags? + + digests = tags.map { |tag| tag.digest }.to_set + + digests.all? do |digest| + client.delete_repository_tag(self.path, digest) + end + end + + def self.build_from_path(path) + self.new(project: path.repository_project, + name: path.repository_name) + end + + def self.create_from_path!(path) + build_from_path(path).tap(&:save!) + end + + def self.build_root_repository(project) + self.new(project: project, name: '') + end +end diff --git a/app/models/group.rb b/app/models/group.rb index 60274386103..106084175ff 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -27,11 +27,14 @@ class Group < Namespace validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } + mount_uploader :avatar, AvatarUploader has_many :uploads, as: :model, dependent: :destroy after_create :post_create_hook after_destroy :post_destroy_hook + after_save :update_two_factor_requirement class << self # Searches for groups matching the given query. @@ -223,4 +226,12 @@ class Group < Namespace type: public? ? 'O' : 'I' # Open vs Invite-only } end + + protected + + def update_two_factor_requirement + return unless require_two_factor_authentication_changed? || two_factor_grace_period_changed? + + users.find_each(&:update_two_factor_requirement) + end end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 446f9f8f8a7..483425cd30f 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -3,11 +3,16 @@ class GroupMember < Member belongs_to :group, foreign_key: 'source_id' + delegate :update_two_factor_requirement, to: :user + # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE validates :source_type, format: { with: /\ANamespace\z/ } default_scope { where(source_type: SOURCE_TYPE) } + after_create :update_two_factor_requirement, unless: :invite? + after_destroy :update_two_factor_requirement, unless: :invite? + def self.access_level_roles Gitlab::Access.options_with_owner end diff --git a/app/models/project.rb b/app/models/project.rb index 1f95d00baf8..639615b91a2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -116,6 +116,7 @@ class Project < ActiveRecord::Base has_one :mock_ci_service, dependent: :destroy has_one :mock_deployment_service, dependent: :destroy has_one :mock_monitoring_service, dependent: :destroy + has_one :microsoft_teams_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -159,6 +160,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete + has_many :container_repositories, dependent: :destroy has_many :commit_statuses, dependent: :destroy has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline' @@ -170,6 +172,8 @@ class Project < ActiveRecord::Base has_many :environments, dependent: :destroy has_many :deployments, dependent: :destroy + has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature @@ -258,6 +262,8 @@ class Project < ActiveRecord::Base scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) } + enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } + # project features may be "disabled", "internal" or "enabled". If "internal", # they are only available to team members. This scope returns projects where # the feature is either enabled, or internal with permission for the user. @@ -406,32 +412,15 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end - def container_registry_path_with_namespace - path_with_namespace.downcase - end - - def container_registry_repository - return unless Gitlab.config.registry.enabled - - @container_registry_repository ||= begin - token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace) - url = Gitlab.config.registry.api_url - host_port = Gitlab.config.registry.host_port - registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) - registry.repository(container_registry_path_with_namespace) - end - end - - def container_registry_repository_url + def container_registry_url if Gitlab.config.registry.enabled - "#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}" + "#{Gitlab.config.registry.host_port}/#{path_with_namespace.downcase}" end end def has_container_registry_tags? - return unless container_registry_repository - - container_registry_repository.tags.any? + container_repositories.to_a.any?(&:has_tags?) || + has_root_container_repository_tags? end def commit(ref = 'HEAD') @@ -922,10 +911,10 @@ class Project < ActiveRecord::Base expire_caches_before_rename(old_path_with_namespace) if has_container_registry_tags? - Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" + Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present!" - # we currently doesn't support renaming repository if it contains tags in container registry - raise StandardError.new('Project cannot be renamed, because tags are present in its container registry') + # we currently doesn't support renaming repository if it contains images in container registry + raise StandardError.new('Project cannot be renamed, because images are present in its container registry') end if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) @@ -1100,25 +1089,21 @@ class Project < ActiveRecord::Base end def shared_runners - shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none + @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none end - def any_runners?(&block) - if runners.active.any?(&block) - return true - end + def active_shared_runners + @active_shared_runners ||= shared_runners.active + end - shared_runners.active.any?(&block) + def any_runners?(&block) + active_runners.any?(&block) || active_shared_runners.any?(&block) end def valid_runners_token?(token) self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end - def build_coverage_enabled? - build_coverage_regex.present? - end - def build_timeout_in_minutes build_timeout / 60 end @@ -1272,7 +1257,7 @@ class Project < ActiveRecord::Base ] if container_registry_enabled? - variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_repository_url, public: true } + variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true } end variables @@ -1405,4 +1390,15 @@ class Project < ActiveRecord::Base Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace) end + + ## + # This method is here because of support for legacy container repository + # which has exactly the same path like project does, but which might not be + # persisted in `container_repositories` table. + # + def has_root_container_repository_tags? + return false unless Gitlab.config.registry.enabled + + ContainerRepository.build_root_repository(self).has_tags? + end end diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index 86d271a3f69..7621a5fa2d8 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -2,11 +2,23 @@ require 'slack-notifier' module ChatMessage class BaseMessage + attr_reader :markdown + attr_reader :user_name + attr_reader :user_avatar + attr_reader :project_name + attr_reader :project_url + def initialize(params) - raise NotImplementedError + @markdown = params[:markdown] || false + @project_name = params.dig(:project, :path_with_namespace) || params[:project_name] + @project_url = params.dig(:project, :web_url) || params[:project_url] + @user_name = params.dig(:user, :username) || params[:user_name] + @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar] end def pretext + return message if markdown + format(message) end @@ -17,6 +29,10 @@ module ChatMessage raise NotImplementedError end + def activity + raise NotImplementedError + end + private def message diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index 791e5b0cec7..4b9a2b1e1f3 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -1,9 +1,6 @@ module ChatMessage class IssueMessage < BaseMessage - attr_reader :user_name attr_reader :title - attr_reader :project_name - attr_reader :project_url attr_reader :issue_iid attr_reader :issue_url attr_reader :action @@ -11,9 +8,7 @@ module ChatMessage attr_reader :description def initialize(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] + super obj_attr = params[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) @@ -27,15 +22,24 @@ module ChatMessage def attachments return [] unless opened_issue? + return description if markdown description_message end + def activity + { + title: "Issue #{state} by #{user_name}", + subtitle: "in #{project_link}", + text: issue_link, + image: user_avatar + } + end + private def message - case state - when "opened" + if state == 'opened' "[#{project_link}] Issue #{state} by #{user_name}" else "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" @@ -64,7 +68,7 @@ module ChatMessage end def issue_title - "##{issue_iid} #{title}" + "#{Issue.reference_prefix}#{issue_iid} #{title}" end end end diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index 5e5efca7bec..7d0de81cdf0 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -1,36 +1,36 @@ module ChatMessage class MergeMessage < BaseMessage - attr_reader :user_name - attr_reader :project_name - attr_reader :project_url - attr_reader :merge_request_id + attr_reader :merge_request_iid attr_reader :source_branch attr_reader :target_branch attr_reader :state attr_reader :title def initialize(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] + super obj_attr = params[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) - @merge_request_id = obj_attr[:iid] + @merge_request_iid = obj_attr[:iid] @source_branch = obj_attr[:source_branch] @target_branch = obj_attr[:target_branch] @state = obj_attr[:state] @title = format_title(obj_attr[:title]) end - def pretext - format(message) - end - def attachments [] end + def activity + { + title: "Merge Request #{state} by #{user_name}", + subtitle: "in #{project_link}", + text: merge_request_link, + image: user_avatar + } + end + private def format_title(title) @@ -50,11 +50,15 @@ module ChatMessage end def merge_request_link - link("merge request !#{merge_request_id}", merge_request_url) + link(merge_request_title, merge_request_url) + end + + def merge_request_title + "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}" end def merge_request_url - "#{project_url}/merge_requests/#{merge_request_id}" + "#{project_url}/merge_requests/#{merge_request_iid}" end end end diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb index 552113bac29..2da4c244229 100644 --- a/app/models/project_services/chat_message/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -1,70 +1,74 @@ module ChatMessage class NoteMessage < BaseMessage - attr_reader :message - attr_reader :user_name - attr_reader :project_name - attr_reader :project_url attr_reader :note attr_reader :note_url + attr_reader :title + attr_reader :target def initialize(params) - params = HashWithIndifferentAccess.new(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] + super + params = HashWithIndifferentAccess.new(params) obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) @note = obj_attr[:note] @note_url = obj_attr[:url] - noteable_type = obj_attr[:noteable_type] - - case noteable_type - when "Commit" - create_commit_note(HashWithIndifferentAccess.new(params[:commit])) - when "Issue" - create_issue_note(HashWithIndifferentAccess.new(params[:issue])) - when "MergeRequest" - create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) - when "Snippet" - create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) - end + @target, @title = case obj_attr[:noteable_type] + when "Commit" + create_commit_note(params[:commit]) + when "Issue" + create_issue_note(params[:issue]) + when "MergeRequest" + create_merge_note(params[:merge_request]) + when "Snippet" + create_snippet_note(params[:snippet]) + end end def attachments + return note if markdown + description_message end + def activity + { + title: "#{user_name} #{link('commented on ' + target, note_url)}", + subtitle: "in #{project_link}", + text: formatted_title, + image: user_avatar + } + end + private + def message + "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*" + end + def format_title(title) title.lines.first.chomp end - def create_commit_note(commit) - commit_sha = commit[:id] - commit_sha = Commit.truncate_sha(commit_sha) - commented_on_message( - "commit #{commit_sha}", - format_title(commit[:message])) + def formatted_title + format_title(title) end def create_issue_note(issue) - commented_on_message( - "issue ##{issue[:iid]}", - format_title(issue[:title])) + ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]] + end + + def create_commit_note(commit) + commit_sha = Commit.truncate_sha(commit[:id]) + + ["commit #{commit_sha}", commit[:message]] end def create_merge_note(merge_request) - commented_on_message( - "merge request !#{merge_request[:iid]}", - format_title(merge_request[:title])) + ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]] end def create_snippet_note(snippet) - commented_on_message( - "snippet ##{snippet[:id]}", - format_title(snippet[:title])) + ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]] end def description_message @@ -74,9 +78,5 @@ module ChatMessage def project_link link(project_name, project_url) end - - def commented_on_message(target, title) - @message = "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{title}*" - end end end diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 210027565a8..4628d9b1a7b 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -1,19 +1,22 @@ module ChatMessage class PipelineMessage < BaseMessage - attr_reader :ref_type, :ref, :status, :project_name, :project_url, - :user_name, :duration, :pipeline_id + attr_reader :ref_type + attr_reader :ref + attr_reader :status + attr_reader :duration + attr_reader :pipeline_id def initialize(data) + super + + @user_name = data.dig(:user, :name) || 'API' + pipeline_attributes = data[:object_attributes] @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' @ref = pipeline_attributes[:ref] @status = pipeline_attributes[:status] @duration = pipeline_attributes[:duration] @pipeline_id = pipeline_attributes[:id] - - @project_name = data[:project][:path_with_namespace] - @project_url = data[:project][:web_url] - @user_name = (data[:user] && data[:user][:name]) || 'API' end def pretext @@ -25,17 +28,24 @@ module ChatMessage end def attachments + return message if markdown + [{ text: format(message), color: attachment_color }] end + def activity + { + title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}", + subtitle: "in #{project_link}", + text: "in #{duration} #{time_measure}", + image: user_avatar || '' + } + end + private def message - "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) + "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{time_measure}" end def humanized_status @@ -74,5 +84,9 @@ module ChatMessage def pipeline_link "[##{pipeline_id}](#{pipeline_url})" end + + def time_measure + 'second'.pluralize(duration) + end end end diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index 2d73b71ec37..c52dd6ef8ef 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -3,33 +3,43 @@ module ChatMessage attr_reader :after attr_reader :before attr_reader :commits - attr_reader :project_name - attr_reader :project_url attr_reader :ref attr_reader :ref_type - attr_reader :user_name def initialize(params) + super + @after = params[:after] @before = params[:before] @commits = params.fetch(:commits, []) - @project_name = params[:project_name] - @project_url = params[:project_url] @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' @ref = Gitlab::Git.ref_name(params[:ref]) - @user_name = params[:user_name] - end - - def pretext - format(message) end def attachments return [] if new_branch? || removed_branch? + return commit_messages if markdown commit_message_attachments end + def activity + action = if new_branch? + "created" + elsif removed_branch? + "removed" + else + "pushed to" + end + + { + title: "#{user_name} #{action} #{ref_type}", + subtitle: "in #{project_link}", + text: compare_link, + image: user_avatar + } + end + private def message @@ -59,7 +69,7 @@ module ChatMessage end def commit_messages - commits.map { |commit| compose_commit_message(commit) }.join("\n") + commits.map { |commit| compose_commit_message(commit) }.join("\n\n") end def commit_message_attachments diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb index 134083e4504..a139a8ee727 100644 --- a/app/models/project_services/chat_message/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -1,17 +1,12 @@ module ChatMessage class WikiPageMessage < BaseMessage - attr_reader :user_name attr_reader :title - attr_reader :project_name - attr_reader :project_url attr_reader :wiki_page_url attr_reader :action attr_reader :description def initialize(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] + super obj_attr = params[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) @@ -29,9 +24,20 @@ module ChatMessage end def attachments + return description if markdown + description_message end + def activity + { + title: "#{user_name} #{action} #{wiki_page_link}", + subtitle: "in #{project_link}", + text: title, + image: user_avatar + } + end + private def message diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 75834103db5..fa782c6fbb7 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -49,10 +49,7 @@ class ChatNotificationService < Service object_kind = data[:object_kind] - data = data.merge( - project_url: project_url, - project_name: project_name - ) + data = custom_data(data) # WebHook events often have an 'update' event that follows a 'open' or # 'close' action. Ignore update events for now to prevent duplicate @@ -68,8 +65,7 @@ class ChatNotificationService < Service opts[:channel] = channel_name if channel_name opts[:username] = username if username - notifier = Slack::Notifier.new(webhook, opts) - notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) + return false unless notify(message, opts) true end @@ -92,6 +88,18 @@ class ChatNotificationService < Service private + def notify(message, opts) + Slack::Notifier.new(webhook, opts).ping( + message.pretext, + attachments: message.attachments, + fallback: message.fallback + ) + end + + def custom_data(data) + data.merge(project_url: project_url, project_name: project_name) + end + def get_message(object_kind, data) case object_kind when "push", "tag_push" diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb new file mode 100644 index 00000000000..9b218fd81b4 --- /dev/null +++ b/app/models/project_services/microsoft_teams_service.rb @@ -0,0 +1,56 @@ +class MicrosoftTeamsService < ChatNotificationService + def title + 'Microsoft Teams Notification' + end + + def description + 'Receive event notifications in Microsoft Teams' + end + + def self.to_param + 'microsoft_teams' + end + + def help + 'This service sends notifications about projects events to Microsoft Teams channels.<br /> + To set up this service: + <ol> + <li><a href="https://msdn.microsoft.com/en-us/microsoft-teams/connectors">Getting started with 365 Office Connectors For Microsoft Teams</a>.</li> + <li>Paste the <strong>Webhook URL</strong> into the field below.</li> + <li>Select events below to enable notifications.</li> + </ol>' + end + + def webhook_placeholder + 'https://outlook.office.com/webhook/…' + end + + def event_field(event) + end + + def default_channel_placeholder + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + { type: 'checkbox', name: 'notify_only_default_branch' }, + ] + end + + private + + def notify(message, opts) + MicrosoftTeams::Notifier.new(webhook).ping( + title: message.project_name, + pretext: message.pretext, + activity: message.activity, + attachments: message.attachments + ) + end + + def custom_data(data) + super(data).merge(markdown: true) + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index dc1c1fab880..1293cb1d486 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -6,6 +6,8 @@ class Repository attr_accessor :path_with_namespace, :project + delegate :ref_name_for_sha, to: :raw_repository + CommitError = Class.new(StandardError) CreateTreeError = Class.new(StandardError) @@ -700,14 +702,6 @@ class Repository end end - def ref_name_for_sha(ref_path, sha) - args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha}) - - # Not found -> ["", 0] - # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] - Gitlab::Popen.popen(args, path_to_repo).first.split.last - end - def refs_contains_sha(ref_type, sha) args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first diff --git a/app/models/service.rb b/app/models/service.rb index 5a0ec58d193..dc76bf925d3 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -237,6 +237,7 @@ class Service < ActiveRecord::Base slack_slash_commands slack teamcity + microsoft_teams ] if Rails.env.development? service_names += %w[mock_ci mock_deployment mock_monitoring] diff --git a/app/models/user.rb b/app/models/user.rb index 95a766f2ede..87eeee204f8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -89,7 +89,8 @@ class User < ActiveRecord::Base has_many :subscriptions, dependent: :destroy has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy - has_one :abuse_report, dependent: :destroy + has_one :abuse_report, dependent: :destroy, foreign_key: :user_id + has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" has_many :spam_logs, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline' @@ -484,6 +485,14 @@ class User < ActiveRecord::Base Group.member_descendants(id) end + def all_expanded_groups + Group.member_hierarchy(id) + end + + def expanded_groups_requiring_two_factor_authentication + all_expanded_groups.where(require_two_factor_authentication: true) + end + def nested_groups_projects Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL'). member_descendants(id) @@ -955,6 +964,15 @@ class User < ActiveRecord::Base self.admin = (new_level == 'admin') end + def update_two_factor_requirement + periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period) + + self.require_two_factor_authentication_from_group = periods.any? + self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period'] + + save + end + protected # override, from Devise::Validatable diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb index ed72ed14d72..c495c3f39bb 100644 --- a/app/presenters/ci/build_presenter.rb +++ b/app/presenters/ci/build_presenter.rb @@ -11,5 +11,11 @@ module Ci def erased_by_name erased_by.name if erased_by_user? end + + def status_title + if auto_canceled? + "Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" + end + end end end diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb new file mode 100644 index 00000000000..a542bdd8295 --- /dev/null +++ b/app/presenters/ci/pipeline_presenter.rb @@ -0,0 +1,11 @@ +module Ci + class PipelinePresenter < Gitlab::View::Presenter::Delegated + presents :pipeline + + def status_title + if auto_canceled? + "Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" + end + end + end +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 3f16dd66d54..ad8b4d43e8f 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -69,13 +69,13 @@ class PipelineEntity < Grape::Entity alias_method :pipeline, :object def can_retry? - pipeline.retryable? && - can?(request.user, :update_pipeline, pipeline) + can?(request.user, :update_pipeline, pipeline) && + pipeline.retryable? end def can_cancel? - pipeline.cancelable? && - can?(request.user, :update_pipeline, pipeline) + can?(request.user, :update_pipeline, pipeline) && + pipeline.cancelable? end def detailed_status diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 7829df9fada..e7a9df8ac4e 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -13,7 +13,15 @@ class PipelineSerializer < BaseSerializer def represent(resource, opts = {}) if resource.is_a?(ActiveRecord::Relation) - resource = resource.includes(project: :namespace) + resource = resource.preload([ + :retryable_builds, + :cancelable_statuses, + :trigger_requests, + :project, + { pending_builds: :project }, + { manual_actions: :project }, + { artifacts: :project } + ]) end if paginated? diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index db82b8f6c30..5e151b0f044 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -17,6 +17,7 @@ module Auth end def self.full_access_token(*names) + names = names.flatten registry = Gitlab.config.registry token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer @@ -37,13 +38,13 @@ module Auth private def authorized_token(*accesses) - token = JSONWebToken::RSAToken.new(registry.key) - token.issuer = registry.issuer - token.audience = params[:service] - token.subject = current_user.try(:username) - token.expire_time = self.class.token_expire_at - token[:access] = accesses.compact - token + JSONWebToken::RSAToken.new(registry.key).tap do |token| + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token.expire_time = self.class.token_expire_at + token[:access] = accesses.compact + end end def scope @@ -55,20 +56,43 @@ module Auth def process_scope(scope) type, name, actions = scope.split(':', 3) actions = actions.split(',') + path = ContainerRegistry::Path.new(name) + return unless type == 'repository' - process_repository_access(type, name, actions) + process_repository_access(type, path, actions) end - def process_repository_access(type, name, actions) - requested_project = Project.find_by_full_path(name) + def process_repository_access(type, path, actions) + return unless path.valid? + + requested_project = path.repository_project + return unless requested_project actions = actions.select do |action| can_access?(requested_project, action) end - { type: type, name: name, actions: actions } if actions.present? + return unless actions.present? + + # At this point user/build is already authenticated. + # + ensure_container_repository!(path, actions) + + { type: type, name: path.to_s, actions: actions } + end + + ## + # Because we do not have two way communication with registry yet, + # we create a container repository image resource when push to the + # registry is successfuly authorized. + # + def ensure_container_repository!(path, actions) + return if path.has_repository? + return unless actions.include?('push') + + ContainerRepository.create_from_path!(path) end def can_access?(requested_project, requested_action) @@ -101,6 +125,11 @@ module Auth can?(current_user, :read_container_image, requested_project) end + ## + # We still support legacy pipeline triggers which do not have associated + # actor. New permissions model and new triggers are always associated with + # an actor, so this should be improved in 10.0 version of GitLab. + # def build_can_push?(requested_project) # Build can push only to the project from which it originates has_authentication_ability?(:build_create_container_image) && @@ -113,14 +142,11 @@ module Auth end def error(code, status:, message: '') - { - errors: [{ code: code, message: message }], - http_status: status - } + { errors: [{ code: code, message: message }], http_status: status } end def has_authentication_ability?(capability) - (@authentication_abilities || []).include?(capability) + @authentication_abilities.to_a.include?(capability) end end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 38a85e9fc42..21350be5557 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -53,6 +53,8 @@ module Ci .execute(pipeline) end + cancel_pending_pipelines if project.auto_cancel_pending_pipelines? + pipeline.tap(&:process!) end @@ -63,6 +65,22 @@ module Ci pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i end + def cancel_pending_pipelines + Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables| + cancelables.find_each do |cancelable| + cancelable.auto_cancel_running(pipeline) + end + end + end + + def auto_cancelable_pipelines + project.pipelines + .where(ref: pipeline.ref) + .where.not(id: pipeline.id) + .where.not(sha: project.repository.sha_from_ref(pipeline.ref)) + .created_or_pending + end + def commit @commit ||= project.commit(origin_sha || origin_ref) end diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb new file mode 100644 index 00000000000..c0e4a798f6a --- /dev/null +++ b/app/services/ci/expire_pipeline_cache_service.rb @@ -0,0 +1,49 @@ +module Ci + class ExpirePipelineCacheService < BaseService + attr_reader :pipeline + + def execute(pipeline) + @pipeline = pipeline + store = Gitlab::EtagCaching::Store.new + + store.touch(project_pipelines_path) + store.touch(commit_pipelines_path) if pipeline.commit + store.touch(new_merge_request_pipelines_path) + merge_requests_pipelines_paths.each { |path| store.touch(path) } + end + + private + + def project_pipelines_path + Gitlab::Routing.url_helpers.namespace_project_pipelines_path( + project.namespace, + project, + format: :json) + end + + def commit_pipelines_path + Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path( + project.namespace, + project, + pipeline.commit.id, + format: :json) + end + + def new_merge_request_pipelines_path + Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path( + project.namespace, + project, + format: :json) + end + + def merge_requests_pipelines_paths + pipeline.merge_requests.collect do |merge_request| + Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path( + project.namespace, + project, + merge_request, + format: :json) + end + end + end +end diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index f72ddbf690c..ecc6173a96a 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -7,9 +7,7 @@ module Ci raise Gitlab::Access::AccessDeniedError end - pipeline.builds.latest.failed_or_canceled.find_each do |build| - next unless build.retryable? - + pipeline.retryable_builds.find_each do |build| Ci::RetryBuildService.new(project, current_user) .reprocess(build) end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index a7142d5950e..06d8d143231 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -31,16 +31,16 @@ module Projects project.team.truncate project.destroy! - unless remove_registry_tags - raise_error('Failed to remove project container registry. Please try again or contact administrator') + unless remove_legacy_registry_tags + raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') end unless remove_repository(repo_path) - raise_error('Failed to remove project repository. Please try again or contact administrator') + raise_error('Failed to remove project repository. Please try again or contact administrator.') end unless remove_repository(wiki_path) - raise_error('Failed to remove wiki repository. Please try again or contact administrator') + raise_error('Failed to remove wiki repository. Please try again or contact administrator.') end end @@ -68,10 +68,16 @@ module Projects end end - def remove_registry_tags + ## + # This method makes sure that we correctly remove registry tags + # for legacy image repository (when repository path equals project path). + # + def remove_legacy_registry_tags return true unless Gitlab.config.registry.enabled - project.container_registry_repository.delete_tags + ContainerRepository.build_root_repository(project).tap do |repository| + return repository.has_tags? ? repository.delete_tags! : true + end end def raise_error(message) diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index a3b32a71a64..ba58b174cc0 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -26,7 +26,7 @@ module Users ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute end - move_issues_to_ghost_user(user) + MigrateToGhostUserService.new(user).execute # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing namespace = user.namespace @@ -35,22 +35,5 @@ module Users user_data end - - private - - def move_issues_to_ghost_user(user) - # Block the user before moving issues to prevent a data race. - # If the user creates an issue after `move_issues_to_ghost_user` - # runs and before the user is destroyed, the destroy will fail with - # an exception. We block the user so that issues can't be created - # after `move_issues_to_ghost_user` runs and before the destroy happens. - user.block - - ghost_user = User.ghost - - user.issues.update_all(author_id: ghost_user.id) - - user.reload - end end end diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb new file mode 100644 index 00000000000..1e1ed1791ec --- /dev/null +++ b/app/services/users/migrate_to_ghost_user_service.rb @@ -0,0 +1,59 @@ +# When a user is destroyed, some of their associated records are +# moved to a "Ghost User", to prevent these associated records from +# being destroyed. +# +# For example, all the issues/MRs a user has created are _not_ destroyed +# when the user is destroyed. +module Users + class MigrateToGhostUserService + extend ActiveSupport::Concern + + attr_reader :ghost_user, :user + + def initialize(user) + @user = user + end + + def execute + # Block the user before moving records to prevent a data race. + # For example, if the user creates an issue after `migrate_issues` + # runs and before the user is destroyed, the destroy will fail with + # an exception. + user.block + + user.transaction do + @ghost_user = User.ghost + + migrate_issues + migrate_merge_requests + migrate_notes + migrate_abuse_reports + migrate_award_emoji + end + + user.reload + end + + private + + def migrate_issues + user.issues.update_all(author_id: ghost_user.id) + end + + def migrate_merge_requests + user.merge_requests.update_all(author_id: ghost_user.id) + end + + def migrate_notes + user.notes.update_all(author_id: ghost_user.id) + end + + def migrate_abuse_reports + user.reported_abuse_reports.update_all(reporter_id: ghost_user.id) + end + + def migrate_award_emoji + user.award_emoji.update_all(user_id: ghost_user.id) + end + end +end diff --git a/app/validators/cron_timezone_validator.rb b/app/validators/cron_timezone_validator.rb new file mode 100644 index 00000000000..542c7d006ad --- /dev/null +++ b/app/validators/cron_timezone_validator.rb @@ -0,0 +1,9 @@ +# CronTimezoneValidator +# +# Custom validator for CronTimezone. +class CronTimezoneValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + cron_parser = Gitlab::Ci::CronParser.new(record.cron, record.cron_timezone) + record.errors.add(attribute, " is invalid syntax") unless cron_parser.cron_timezone_valid? + end +end diff --git a/app/validators/cron_validator.rb b/app/validators/cron_validator.rb new file mode 100644 index 00000000000..981fade47a6 --- /dev/null +++ b/app/validators/cron_validator.rb @@ -0,0 +1,9 @@ +# CronValidator +# +# Custom validator for Cron. +class CronValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + cron_parser = Gitlab::Ci::CronParser.new(record.cron, record.cron_timezone) + record.errors.add(attribute, " is invalid syntax") unless cron_parser.cron_valid? + end +end diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 05f3d9a3b50..18c6c559049 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -30,5 +30,5 @@ = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-block" - else .btn.btn-sm.disabled.btn-block - Already Blocked + Already blocked = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr" diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 5d51a2b5cbc..703f611bb45 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -148,7 +148,7 @@ Sign-in enabled - if omniauth_enabled? && button_based_providers.any? .form-group - = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' + = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2' .col-sm-10 .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes.each do |source| diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index b3a3b4c1d45..eb4293c7e37 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -4,7 +4,7 @@ %p.light System OAuth applications don't belong to any user and can only be managed by admins %hr -%p= link_to 'New Application', new_admin_application_path, class: 'btn btn-success' +%p= link_to 'New application', new_admin_application_path, class: 'btn btn-success' %table.table.table-striped %thead %tr diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index ebca9beb035..8c9fdc9ae42 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -125,7 +125,7 @@ = link_to admin_projects_path do %h1= number_with_delimiter(Project.cached_count) %hr - = link_to('New Project', new_project_path, class: "btn btn-new") + = link_to('New project', new_project_path, class: "btn btn-new") .col-sm-4 .light-well.well-centered %h4 Users @@ -133,7 +133,7 @@ = link_to admin_users_path do %h1= number_with_delimiter(User.count) %hr - = link_to 'New User', new_admin_user_path, class: "btn btn-new" + = link_to 'New user', new_admin_user_path, class: "btn btn-new" .col-sm-4 .light-well.well-centered %h4 Groups @@ -141,7 +141,7 @@ = link_to admin_groups_path do %h1= number_with_delimiter(Group.count) %hr - = link_to 'New Group', new_admin_group_path, class: "btn btn-new" + = link_to 'New group', new_admin_group_path, class: "btn btn-new" .row.prepend-top-10 .col-md-4 diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 7b71bb5b287..007da8c1d29 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -3,7 +3,7 @@ %h3.page-title.deploy-keys-title Public deploy keys (#{@deploy_keys.count}) .pull-right - = link_to 'New Deploy Key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted' + = link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted' - if @deploy_keys.any? .table-holder.deploy-keys-list diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 589f4557b52..d9f05003904 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -13,7 +13,7 @@ .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f - = render 'groups/group_lfs_settings', f: f + = render 'groups/group_admin_settings', f: f - if @group.new_record? .form-group diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 07775247cfd..e5f380c78e2 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -30,7 +30,7 @@ = link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do = sort_title_largest_group = link_to new_admin_group_path, class: "btn btn-new" do - New Group + New group %ul.content-list = render @groups diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 30b3fabdd7e..9149b8e7fb9 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -116,7 +116,7 @@ group members %span.badge= @group.members.size .pull-right - = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@group, :members]), class: "btn btn-xs" + = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-xs" %ul.well-list.group-users-list.content-list = render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false } .panel-footer diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 551edf14361..d9c7948763a 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -51,7 +51,7 @@ = f.check_box :enable_ssl_verification %strong Enable SSL verification .form-actions - = f.submit "Add System Hook", class: "btn btn-create" + = f.submit "Add system hook", class: "btn btn-create" %hr - if @hooks.any? @@ -62,7 +62,7 @@ - @hooks.each do |hook| %li .controls - = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-sm" + = link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm" = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" .monospace= hook.url %div diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml index 741d111fb7d..ff67e59cdac 100644 --- a/app/views/admin/identities/index.html.haml +++ b/app/views/admin/identities/index.html.haml @@ -1,7 +1,7 @@ - page_title "Identities", @user.name, "Users" = render 'admin/users/head' -= link_to 'New Identity', new_admin_user_identity_path, class: 'pull-right btn btn-new' += link_to 'New identity', new_admin_user_identity_path, class: 'pull-right btn btn-new' - if @identities.present? .table-holder %table.table diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 2967da6e692..08a8f627113 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -159,7 +159,7 @@ %span.badge= @group_members.size .pull-right = link_to admin_group_path(@group), class: 'btn btn-xs' do - = icon('pencil-square-o', text: 'Manage Access') + = icon('pencil-square-o', text: 'Manage access') %ul.well-list.content-list = render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false } .panel-footer @@ -173,7 +173,7 @@ project members %span.badge= @project.users.size .pull-right - = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@project, :members]), class: "btn btn-xs" + = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-xs" %ul.well-list.project_members.content-list = render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false } .panel-footer diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml index 33f6d847782..ea6a0c4fb77 100644 --- a/app/views/admin/spam_logs/_spam_log.html.haml +++ b/app/views/admin/spam_logs/_spam_log.html.haml @@ -35,5 +35,5 @@ = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" - else .btn.btn-xs.disabled - Already Blocked + Already blocked = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index a756cb7243a..8862455688f 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -37,6 +37,6 @@ - if user.can_be_removed? && can?(current_user, :destroy_user, @user) %li.divider %li - = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, + = link_to 'Delete user', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, class: 'btn btn-remove btn-block', method: :delete diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 298cf0fa950..c7cd86527d3 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -33,7 +33,7 @@ = sort_title_recently_updated = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do = sort_title_oldest_updated - = link_to 'New User', new_admin_user_path, class: 'btn btn-new btn-search' + = link_to 'New user', new_admin_user_path, class: 'btn btn-new btn-search' .nav-block %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 5aae410a63f..3ca45fbf751 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -13,5 +13,7 @@ %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', 'aria-label': 'Add emoji', data: { title: 'Add emoji', placement: "bottom" } } - = icon('smile-o', class: "award-control-icon award-control-icon-normal") + %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') + %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') + %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile') = icon('spinner spin', class: "award-control-icon award-control-icon-loading") diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml index c00c7f7407e..39c7fb0eba2 100644 --- a/app/views/ci/status/_badge.html.haml +++ b/app/views/ci/status/_badge.html.haml @@ -1,12 +1,13 @@ - status = local_assigns.fetch(:status) -- link = local_assigns.fetch(:link, true) -- css_classes = "ci-status ci-#{status.group}" +- link = local_assigns.fetch(:link, true) +- title = local_assigns.fetch(:title, nil) +- css_classes = "ci-status ci-#{status.group} #{'has-tooltip' if title.present?}" - if link && status.has_details? - = link_to status.details_path, class: css_classes do + = link_to status.details_path, class: css_classes, title: title do = custom_icon(status.icon) = status.text - else - %span{ class: css_classes } + %span{ class: css_classes, title: title } = custom_icon(status.icon) = status.text diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 13eaba41f4c..0e848386ebb 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -11,4 +11,4 @@ = render 'shared/groups/dropdown' - if current_user.can_create_group? = link_to new_group_path, class: "btn btn-new" do - New Group + New group diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 4679b9549d1..64b737ee886 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -19,4 +19,4 @@ = render 'shared/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do - New Project + New project diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 10867140d4f..faa68468043 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -8,7 +8,7 @@ .nav-controls = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do = icon('rss') - = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue" = render 'shared/issuable/filter', type: :issues = render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index e64c78c4cb8..12966c01950 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -4,7 +4,7 @@ .top-area = render 'shared/issuable/nav', type: :merge_requests .nav-controls - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request" = render 'shared/issuable/filter', type: :merge_requests = render 'shared/merge_requests' diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 505b475f55b..664ec618b79 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -5,7 +5,7 @@ = render 'shared/milestones_filter', counts: @milestone_states .nav-controls - = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New Milestone', include_groups: true + = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true .milestones %ul.content-list diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index a0bd14df209..53a33adc14d 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,8 +3,6 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = author_avatar(event, size: 40) - - if event.created_project? = render "events/event/created_project", event: event - elsif event.push? diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index a1a282178e7..1584695a62b 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -10,5 +10,5 @@ #{time_ago_with_tooltip(event.created_at)} .pull-right - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do - Create Merge Request + = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do + Create merge request diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 2fb6b5647da..c0c11d00519 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,5 +1,15 @@ +- if event.target + - if event.action_name == "opened" + .profile-icon.open-icon + = custom_icon("icon_status_open") + - elsif event.action_name == "closed" + .profile-icon.closed-icon + = custom_icon("icon_status_closed") + - else + .profile-icon.fork-icon + = custom_icon("icon_code_fork") + .event-title - %span.author_name= link_to_author event %span{ class: event.action_name } - if event.target = event.action_name diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 80cf2344fe1..340d8c61026 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,5 +1,7 @@ +.profile-icon.open-icon + = custom_icon("icon_status_open") + .event-title - %span.author_name= link_to_author event %span{ class: event.action_name } = event_action_name(event) diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 64b5a733b77..cbcfe0ff71f 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,5 +1,7 @@ +.profile-icon + = custom_icon("icon_comment_o") + .event-title - %span.author_name= link_to_author event = event.action_name = event_note_title_html(event) diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index efd13aabf20..1583f380737 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -1,7 +1,12 @@ - project = event.project +.profile-icon + - if event.action_name == "deleted" + = custom_icon("trash_o") + - else + = custom_icon("icon_commit") + .event-title - %span.author_name= link_to_author event %span.pushed #{event.action_name} #{event.ref_type} %strong - commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name) @@ -48,4 +53,3 @@ .event-body %ul.well-list.event_commits = render "events/commit", commit: last_commit, project: project, event: event - diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml new file mode 100644 index 00000000000..2ace1e2dd1e --- /dev/null +++ b/app/views/groups/_group_admin_settings.html.haml @@ -0,0 +1,28 @@ +- if current_user.admin? + .form-group + = f.label :lfs_enabled, 'Large File Storage', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :lfs_enabled do + = f.check_box :lfs_enabled, checked: @group.lfs_enabled? + %strong + Allow projects within this group to use Git LFS + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + %br/ + %span.descr This setting can be overridden in each project. + +- if can? current_user, :admin_group, @group + .form-group + = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :require_two_factor_authentication do + = f.check_box :require_two_factor_authentication + %strong + Require all users in this group to setup Two-factor authentication + = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group') + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.text_field :two_factor_grace_period, class: 'form-control' + .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml deleted file mode 100644 index 3c622ca5c3c..00000000000 --- a/app/views/groups/_group_lfs_settings.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- if current_user.admin? - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :lfs_enabled do - = f.check_box :lfs_enabled, checked: @group.lfs_enabled? - %strong - Allow projects within this group to use Git LFS - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - %br/ - %span.descr This setting can be overridden in each project. diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 80a77dab97f..7d5add3cc1c 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -27,7 +27,7 @@ .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f - = render 'group_lfs_settings', f: f + = render 'group_admin_settings', f: f .form-group %hr @@ -51,4 +51,4 @@ %strong Removed group can not be restored! .form-actions - = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" + = link_to 'Remove group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index f4c17dc2d16..182dbe2f98a 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -11,7 +11,7 @@ = icon('rss') %span.icon-label Subscribe - = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 6893168f039..f91bee0b610 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -7,7 +7,7 @@ .nav-controls - if can?(current_user, :admin_milestones, @group) = link_to new_group_milestone_path(@group), class: "btn btn-new" do - New Milestone + New milestone .row-content-block Only milestones from diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 63cadfca530..8d3aa4d1a74 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -39,5 +39,5 @@ = render "shared/milestones/form_dates", f: f .form-actions - = f.submit 'Create Milestone', class: "btn-create btn" + = f.submit 'Create milestone', class: "btn-create btn" = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel" diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 83bdd654f27..62ad47972b9 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -7,7 +7,7 @@ - if can? current_user, :admin_group, @group .controls = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do - New Project + New project %ul.well-list - @projects.each do |project| %li diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 8e6da3fad90..700c5e61a14 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -17,6 +17,10 @@ %th Global Shortcuts %tr %td.shortcut + .key n + %td Main Navigation + %tr + %td.shortcut .key s %td Focus Search %tr @@ -39,24 +43,46 @@ .key %i.fa.fa-arrow-up %td Edit last comment (when focused on an empty textarea) - %tbody %tr - %th - %th Project Files browsing + %td.shortcut + .key shift t + %td + Go to todos %tr %td.shortcut - .key - %i.fa.fa-arrow-up - %td Move selection up + .key shift a + %td + Go to the activity feed %tr %td.shortcut - .key - %i.fa.fa-arrow-down - %td Move selection down + .key shift p + %td + Go to projects %tr %td.shortcut - .key enter - %td Open Selection + .key shift i + %td + Go to issues + %tr + %td.shortcut + .key shift m + %td + Go to merge requests + %tr + %td.shortcut + .key shift g + %td + Go to groups + %tr + %td.shortcut + .key shift l + %td + Go to milestones + %tr + %td.shortcut + .key shift s + %td + Go to snippets %tbody %tr %th @@ -79,51 +105,8 @@ %td.shortcut .key esc %td Go back - %tbody - %tr - %th - %th Project File - %tr - %td.shortcut - .key y - %td Go to file permalink - .col-lg-4 %table.shortcut-mappings - %tbody.hidden-shortcut.project{ style: 'display:none' } - %tr - %th - %th Global Dashboard - %tr - %td.shortcut - .key g - .key a - %td - Go to the activity feed - %tr - %td.shortcut - .key g - .key p - %td - Go to projects - %tr - %td.shortcut - .key g - .key i - %td - Go to issues - %tr - %td.shortcut - .key g - .key m - %td - Go to merge requests - %tr - %td.shortcut - .key g - .key t - %td - Go to todos %tbody %tr %th @@ -155,7 +138,7 @@ %tr %td.shortcut .key g - .key b + .key j %td Go to jobs %tr @@ -167,7 +150,7 @@ %tr %td.shortcut .key g - .key g + .key d %td Go to repository charts %tr @@ -179,7 +162,7 @@ %tr %td.shortcut .key g - .key l + .key b %td Go to issue boards %tr @@ -196,12 +179,45 @@ Go to snippets %tr %td.shortcut + .key g + .key w + %td + Go to wiki + %tr + %td.shortcut .key t %td Go to finding file %tr %td.shortcut .key i %td New issue + + %tbody + %tr + %th + %th Project Files browsing + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Move selection up + %tr + %td.shortcut + .key + %i.fa.fa-arrow-down + %td Move selection down + %tr + %td.shortcut + .key enter + %td Open Selection + %tbody + %tr + %th + %th Project File + %tr + %td.shortcut + .key y + %td Go to file permalink .col-lg-4 %table.shortcut-mappings %tbody.hidden-shortcut.network{ style: 'display:none' } diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 1fb2c6271ad..207f80bedfe 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -225,7 +225,7 @@ %ul.dropdown-menu %li %a{ href: "#" } - Dropdown Option + Dropdown option .dropdown.inline.pull-right %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown @@ -233,7 +233,7 @@ %ul.dropdown-menu.dropdown-menu-align-right %li %a{ href: "#" } - Dropdown Option + Dropdown option .example %div .dropdown.inline @@ -243,7 +243,7 @@ %ul.dropdown-menu.dropdown-menu-selectable %li %a.is-active{ href: "#" } - Dropdown Option + Dropdown option .example %div .dropdown.inline @@ -262,26 +262,26 @@ %ul %li %a.is-active{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option %li.divider %li %a{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option .dropdown-footer %strong Tip: If an author is not a member of this project, you can still filter by his name while using the search field. @@ -301,26 +301,26 @@ %ul %li %a.is-active{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option %li.divider %li %a{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option %li %a{ href: "#" } - Dropdown Option + Dropdown option .dropdown-footer %strong Tip: If an author is not a member of this project, you can still filter by his name while using the search field. diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index 4c6af0b7908..9c2da3a3eec 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -9,7 +9,7 @@ To import a GitHub project, you first need to authorize GitLab to access the list of your GitHub repositories: - = link_to 'List Your GitHub Repositories', status_import_github_path, class: 'btn btn-success' + = link_to 'List your GitHub repositories', status_import_github_path, class: 'btn btn-success' %hr @@ -28,7 +28,7 @@ = form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do .form-group = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: "Personal Access Token", size: 40 - = submit_tag 'List Your GitHub Repositories', class: 'btn btn-success' + = submit_tag 'List your GitHub repositories', class: 'btn btn-success' - unless github_import_configured? %hr diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 23abf6897d4..7408fe3f6d0 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -29,11 +29,11 @@ - if current_user - if session[:impersonator_id] %li.impersonation - = link_to admin_impersonation_path, method: :delete, title: "Stop Impersonation", aria: { label: 'Stop Impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret fw') - if current_user.is_admin? %li - = link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') - if current_user.can_create_project? %li diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 15285ee32a3..444ecc414c0 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,10 +1,18 @@ %ul = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + .shortcut-mappings + .key + = icon('arrow-up', 'aria-label' => 'hidden') + P %span Projects = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + .shortcut-mappings + .key + = icon('arrow-up', 'aria-label' => 'hidden') + A %span Activity - if koding_enabled? @@ -13,25 +21,45 @@ %span Koding = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do - = link_to dashboard_groups_path, title: 'Groups' do + = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do + .shortcut-mappings + .key + = icon('arrow-up', 'aria-label' => 'hidden') + G %span Groups = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, title: 'Milestones' do + = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do + .shortcut-mappings + .key + = icon('arrow-up', 'aria-label' => 'hidden') + L %span Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do + .shortcut-mappings + .key + = icon('arrow-up', 'aria-label' => 'hidden') + I %span Issues .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do + .shortcut-mappings + .key + = icon('arrow-up', 'aria-label' => 'hidden') + M %span Merge Requests .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) = nav_link(controller: 'dashboard/snippets') do - = link_to dashboard_snippets_path, title: 'Snippets' do + = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do + .shortcut-mappings + .key + = icon('arrow-up', 'aria-label' => 'hidden') + S %span Snippets %li.divider diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 4beb6fcee5d..a83faa839df 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -137,6 +137,6 @@ - if build.has_trace? %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } - = build.trace_html(last_lines: 10).html_safe + = build.trace.html(last_lines: 10).html_safe - else %td{ colspan: "2" } diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb index c1a4ea40cf5..294238eee51 100644 --- a/app/views/notify/pipeline_failed_email.text.erb +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -35,7 +35,7 @@ had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. Stage: <%= build.stage %> Name: <%= build.name %> <% if build.has_trace? -%> -Trace: <%= build.trace_with_state(last_lines: 10)[:text] %> +Trace: <%= build.trace.raw(last_lines: 10) %> <% end -%> <% end -%> diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 5ce2220c907..d843cacd52d 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -49,14 +49,14 @@ %p Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} - if current_user.two_factor_enabled? - = link_to 'Manage Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-info' + = link_to 'Manage two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-info' = link_to 'Disable', profile_two_factor_auth_path, method: :delete, data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." }, class: 'btn btn-danger' - else .append-bottom-10 - = link_to 'Enable Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-success' + = link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success' %hr - if button_based_providers.any? diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index dc499be885b..f5a323dbaf8 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -33,17 +33,17 @@ %li = @primary %span.pull-right - %span.label.label-success Primary Email + %span.label.label-success Primary email - if @primary === current_user.public_email - %span.label.label-info Public Email + %span.label.label-info Public email - if @primary === current_user.notification_email - %span.label.label-info Notification Email + %span.label.label-info Notification email - @emails.each do |email| %li = email.email %span.pull-right - if email.email === current_user.public_email - %span.label.label-info Public Email + %span.label.label-info Public email - if email.email === current_user.notification_email - %span.label.label-info Notification Email + %span.label.label-info Notification email = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-warning prepend-left-10' diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 0645ecad496..c852107e69a 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -19,7 +19,7 @@ Your New Personal Access Token .form-group = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block" - = clipboard_button(clipboard_text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") + = clipboard_button(text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. %hr diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 7ade5f00d47..0ff05098cd7 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -44,7 +44,7 @@ = label_tag :pin_code, nil, class: "label-light" = text_field_tag :pin_code, nil, class: "form-control", required: true .prepend-top-default - = submit_tag 'Register with Two-Factor App', class: 'btn btn-success' + = submit_tag 'Register with two-factor app', class: 'btn btn-success' %hr diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index 640612ca433..b55dc3dce5c 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -1,5 +1,5 @@ .form-actions - = button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create' + = button_tag 'Commit changes', class: 'btn commit-btn js-commit-button btn-create' = link_to 'Cancel', cancel_path, class: 'btn btn-cancel', data: {confirm: leave_edit_message} diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml index dbb33090670..3feb11645a0 100644 --- a/app/views/projects/_find_file_link.html.haml +++ b/app/views/projects/_find_file_link.html.haml @@ -1,3 +1,3 @@ = link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do = icon('search') - %span Find File + %span Find file diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index a08436715d2..768bc1fb323 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -10,9 +10,9 @@ - if @project && event.project != @project %span at %strong= link_to_project event.project - = clipboard_button(clipboard_text: event.ref_name, class: 'btn-clipboard btn-transparent', title: 'Copy branch to clipboard') + = clipboard_button(text: event.ref_name, class: 'btn-clipboard btn-transparent', title: 'Copy branch to clipboard') #{time_ago_with_tooltip(event.created_at)} .pull-right - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do - Create Merge Request + = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do + Create merge request diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 2b2ee6ed987..aa9b852035e 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -25,4 +25,11 @@ #blob-content-holder.blob-content-holder %article.file-holder = render "projects/blob/header", blob: blob + - if current_user + .js-file-fork-suggestion-section.file-fork-suggestion.hidden + %span.file-fork-suggestion-note + You don't have permission to edit this file. Try forking this project to edit the file. + = link_to 'Fork', fork_path, method: :post, class: 'btn btn-grouped btn-inverted btn-new' + %button.js-cancel-fork-suggestion.btn.btn-grouped{ type: 'button' } + Cancel = render blob.to_partial_path(@project), blob: blob diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index deeeae3d64a..828f8e073a9 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -20,7 +20,7 @@ -# only show normal/blame view links for text files - if blob_text_viewable?(blob) - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) - = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), + = link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id), class: 'btn btn-sm' - else = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), @@ -32,8 +32,8 @@ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url' - - if current_user - .btn-group{ role: "group" }< - = edit_blob_link if blob_text_viewable?(blob) + .btn-group{ role: "group" }< + = edit_blob_link if blob_text_viewable?(blob) + - if current_user = replace_blob_link = delete_blob_link diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml index d52733d2bd6..2a178325041 100644 --- a/app/views/projects/blob/_template_selectors.html.haml +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -5,7 +5,7 @@ .template-type-selector.js-template-type-selector-wrap.hidden = dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } ) .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a License template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) + = dropdown_tag("Apply a license template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 9eb610ba9c0..724675e659c 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -21,7 +21,7 @@ .controls.hidden-xs< - if merge_project && create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do - Merge Request + Merge request - if branch.name != @repository.root_ref = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 7eb17e887e7..104db85809c 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,14 +1,16 @@ +- pipeline = @build.pipeline + .content-block.build-header.top-area .header-content - = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false + = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title Job %strong.js-build-id ##{@build.id} in pipeline - = link_to pipeline_path(@build.pipeline) do - %strong ##{@build.pipeline.id} + = link_to pipeline_path(pipeline) do + %strong ##{pipeline.id} for commit - = link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do - %strong= @build.pipeline.short_sha + = link_to namespace_project_commit_path(@project.namespace, @project, pipeline.sha) do + %strong= pipeline.short_sha from = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do %code diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 6f45d5b0689..f4a66398c85 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -68,7 +68,7 @@ - elsif @build.runner \##{@build.runner.id} .btn-group.btn-group-justified{ role: :group } - - if @build.has_trace_file? + - if @build.has_trace? = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' - if @build.active? = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index acfdb250aff..82806f022ee 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -20,6 +20,6 @@ %th Coverage %th - = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, pipeline_link: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin } + = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, pipeline_link: true, stage: true, allow_retry: true, admin: admin } = paginate builds, theme: 'gitlab' diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 5ffc0e20d10..65162aacda1 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -17,7 +17,7 @@ = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do - %span CI Lint + %span CI lint .content-list.builds-content-list = render "table", builds: @builds, project: @project diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index aeed293a724..4700b7a9a45 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -1,109 +1,110 @@ +- job = build.present(current_user: current_user) +- pipeline = job.pipeline - admin = local_assigns.fetch(:admin, false) - ref = local_assigns.fetch(:ref, nil) - commit_sha = local_assigns.fetch(:commit_sha, nil) - retried = local_assigns.fetch(:retried, false) - pipeline_link = local_assigns.fetch(:pipeline_link, false) - stage = local_assigns.fetch(:stage, false) -- coverage = local_assigns.fetch(:coverage, false) - allow_retry = local_assigns.fetch(:allow_retry, false) %tr.build.commit{ class: ('retried' if retried) } %td.status - = render "ci/status/badge", status: build.detailed_status(current_user) + = render "ci/status/badge", status: job.detailed_status(current_user), title: job.status_title %td.branch-commit - - if can?(current_user, :read_build, build) - = link_to namespace_project_build_url(build.project.namespace, build.project, build) do - %span.build-link ##{build.id} + - if can?(current_user, :read_build, job) + = link_to namespace_project_build_url(job.project.namespace, job.project, job) do + %span.build-link ##{job.id} - else - %span.build-link ##{build.id} + %span.build-link ##{job.id} - if ref - - if build.ref + - if job.ref .icon-container - = build.tag? ? icon('tag') : icon('code-fork') - = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" + = job.tag? ? icon('tag') : icon('code-fork') + = link_to job.ref, namespace_project_commits_path(job.project.namespace, job.project, job.ref), class: "monospace branch-name" - else .light none .icon-container.commit-icon = custom_icon("icon_commit") - if commit_sha - = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" + = link_to job.short_sha, namespace_project_commit_path(job.project.namespace, job.project, job.sha), class: "commit-id monospace" - - if build.stuck? + - if job.stuck? = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.') - if retried = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried') .label-container - - if build.tags.any? - - build.tags.each do |tag| + - if job.tags.any? + - job.tags.each do |tag| %span.label.label-primary = tag - - if build.try(:trigger_request) + - if job.try(:trigger_request) %span.label.label-info triggered - - if build.try(:allow_failure) + - if job.try(:allow_failure) %span.label.label-danger allowed to fail - - if build.action? + - if job.action? %span.label.label-info manual - if pipeline_link %td - = link_to pipeline_path(build.pipeline) do - %span.pipeline-id ##{build.pipeline.id} + = link_to pipeline_path(pipeline) do + %span.pipeline-id ##{pipeline.id} %span by - - if build.pipeline.user - = user_avatar(user: build.pipeline.user, size: 20) + - if pipeline.user + = user_avatar(user: pipeline.user, size: 20) - else %span.monospace API - if admin %td - - if build.project - = link_to build.project.name_with_namespace, admin_namespace_project_path(build.project.namespace, build.project) + - if job.project + = link_to job.project.name_with_namespace, admin_namespace_project_path(job.project.namespace, job.project) %td - - if build.try(:runner) - = runner_link(build.runner) + - if job.try(:runner) + = runner_link(job.runner) - else .light none - if stage %td - = build.stage + = job.stage %td - = build.name + = job.name %td - - if build.duration + - if job.duration %p.duration = custom_icon("icon_timer") - = duration_in_numbers(build.duration) + = duration_in_numbers(job.duration) - - if build.finished_at + - if job.finished_at %p.finished-at = icon("calendar") - %span= time_ago_with_tooltip(build.finished_at) + %span= time_ago_with_tooltip(job.finished_at) %td.coverage - - if coverage && build.try(:coverage) - #{build.coverage}% + - if job.try(:coverage) + #{job.coverage}% %td .pull-right - - if can?(current_user, :read_build, build) && build.artifacts? - = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do + - if can?(current_user, :read_build, job) && job.artifacts? + = link_to download_namespace_project_build_artifacts_path(job.project.namespace, job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do = icon('download') - - if can?(current_user, :update_build, build) - - if build.active? - = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do + - if can?(current_user, :update_build, job) + - if job.active? + = link_to cancel_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = icon('remove', class: 'cred') - elsif allow_retry - - if build.playable? && !admin - = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do + - if job.playable? && !admin + = link_to play_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = custom_icon('icon_play') - - elsif build.retryable? - = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do + - elsif job.retryable? + = link_to retry_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do = icon('repeat') diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index a0a292d0508..f604d6e5fbb 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,7 +1,7 @@ .page-content-header .header-main-content %strong Commit #{@commit.short_id} - = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard") + = clipboard_button(text: @commit.id, title: "Copy commit SHA to clipboard") %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} %span by @@ -20,7 +20,7 @@ = icon('comment') = @notes_count = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do - Browse Files + Browse files .dropdown.inline %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } %span Options diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index c2b32a22170..3ee85723ebe 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -47,7 +47,6 @@ %th Job ID %th Name %th - - if pipeline.project.build_coverage_enabled? - %th Coverage + %th Coverage %th = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 4b1ff75541a..8f32d2b72e5 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -37,6 +37,6 @@ .commit-actions.flex-row.hidden-xs - if commit.status(ref) = render_commit_status(commit, ref: ref) - = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") + = clipboard_button(text: commit.id, title: "Copy commit SHA to clipboard") = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 38dbf2ac10b..c1c2fb3d299 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -18,16 +18,16 @@ .block-controls.hidden-xs.hidden-sm - if @merge_request.present? .control - = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' + = link_to "View open merge request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' - elsif create_mr_button?(@repository.root_ref, @ref) .control - = link_to "Create Merge Request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' + = link_to "Create merge request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' .control = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits Feed", class: 'btn' do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits feed", class: 'btn' do = icon("rss") %div{ id: dom_id(@project) } diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 08236216421..0f080b6acee 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -21,6 +21,6 @@ = button_tag "Compare", class: "btn btn-create commits-compare-btn" - if @merge_request.present? - = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn' + = link_to "View open merge request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn' - elsif create_mr_button? - = link_to "Create Merge Request", create_mr_path, class: 'prepend-left-10 btn' + = link_to "Create merge request", create_mr_path, class: 'prepend-left-10 btn' diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml index e27281d6917..b4102fcf103 100644 --- a/app/views/projects/environments/_metrics_button.html.haml +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -1,6 +1,6 @@ - environment = local_assigns.fetch(:environment) -- return unless environment.has_metrics? && can?(current_user, :read_environment, environment) +- return unless can?(current_user, :read_environment, environment) = link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do = icon('area-chart') diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index 4b101447bc0..f7e3733ba0b 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -8,7 +8,4 @@ #environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s, - "css-class" => container_class, - "commit-icon-svg" => custom_icon("icon_commit"), - "terminal-icon-svg" => custom_icon("icon_terminal"), - "play-icon-svg" => custom_icon("icon_play") } } + "css-class" => container_class } } diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 92dc58cd38d..2e54af698aa 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -5,7 +5,7 @@ = page_specific_javascript_bundle_tag('monitoring') = render "projects/pipelines/head" -%div{ class: container_class } +.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" } .top-area .row .col-sm-6 @@ -16,13 +16,68 @@ .col-sm-6 .nav-controls = render 'projects/deployments/actions', deployment: @environment.last_deployment - .row - .col-sm-12 - %h4 - CPU utilization - %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } - .row - .col-sm-12 - %h4 - Memory usage - %svg.prometheus-graph{ 'graph-type' => 'memory_values' } + .prometheus-state + .js-getting-started.hidden + .row + .col-md-4.col-md-offset-4.state-svg + = render "shared/empty_states/monitoring/getting_started.svg" + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Get started with performance monitoring + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. + = link_to help_page_path('administration/monitoring/prometheus/index.md') do + Learn more about performance monitoring + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + = link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'), class: 'btn btn-success' do + Configure Prometheus + .js-loading.hidden + .row + .col-md-4.col-md-offset-4.state-svg + = render "shared/empty_states/monitoring/loading.svg" + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Waiting for performance data + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available. + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + = link_to help_page_path('administration/monitoring/prometheus/index.md'), class: 'btn btn-success' do + View documentation + .js-unable-to-connect.hidden + .row + .col-md-4.col-md-offset-4.state-svg + = render "shared/empty_states/monitoring/unable_to_connect.svg" + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Unable to connect to Prometheus server + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Ensure connectivity is available from the GitLab server to the + = link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus') do + Prometheus server + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + = link_to help_page_path('administration/monitoring/prometheus/index.md'), class:'btn btn-success' do + View documentation + + .prometheus-graphs + .row + .col-sm-12 + %h4 + CPU utilization + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } + .row + .col-sm-12 + %h4 + Memory usage + %svg.prometheus-graph{ 'graph-type' => 'memory_values' } diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml index 98d81308407..524b77783ef 100644 --- a/app/views/projects/forks/error.html.haml +++ b/app/views/projects/forks/error.html.haml @@ -22,4 +22,4 @@ %p = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do %i.fa.fa-code-fork - Try to Fork again + Try to fork again diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 07fb80750d6..f458646522c 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -4,7 +4,6 @@ - retried = local_assigns.fetch(:retried, false) - pipeline_link = local_assigns.fetch(:pipeline_link, false) - stage = local_assigns.fetch(:stage, false) -- coverage = local_assigns.fetch(:coverage, false) %tr.generic_commit_status{ class: ('retried' if retried) } %td.status @@ -80,7 +79,7 @@ %span= time_ago_with_tooltip(generic_commit_status.finished_at) %td.coverage - - if coverage && generic_commit_status.try(:coverage) + - if generic_commit_status.try(:coverage) #{generic_commit_status.coverage}% %td diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml index d2038a2be68..da65157a10b 100644 --- a/app/views/projects/issues/_issue_by_email.html.haml +++ b/app/views/projects/issues/_issue_by_email.html.haml @@ -16,7 +16,7 @@ .email-modal-input-group.input-group = text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true .input-group-btn - = clipboard_button(clipboard_target: '#issue_email') + = clipboard_button(target: '#issue_email') %p The subject will be used as the title of the new issue, and the message will be the description. diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index f3a429d12d9..4ac0bc1d028 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -24,9 +24,9 @@ issue: { assignee_id: issues_finder.assignee.try(:id), milestone_id: issues_finder.milestones.first.try(:id) }), class: "btn btn-new", - title: "New Issue", + title: "New issue", id: "new_issue_link" do - New Issue + New issue = render 'shared/issuable/search_bar', type: :issues .issues-holder diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index e7fcac4c477..03069804c86 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -53,5 +53,6 @@ :javascript var merge_request = new MergeRequest({ - action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}" + action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", + setUrl: false, }); diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index cde0ce08e14..f3372c7657f 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -8,7 +8,7 @@ %p %strong Step 1. Fetch and check out the branch for this merge request - = clipboard_button(clipboard_target: "pre#merge-info-1", title: "Copy commands to clipboard") + = clipboard_button(target: "pre#merge-info-1", title: "Copy commands to clipboard") %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve @@ -25,7 +25,7 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up - = clipboard_button(clipboard_target: "pre#merge-info-3", title: "Copy commands to clipboard") + = clipboard_button(target: "pre#merge-info-3", title: "Copy commands to clipboard") %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve @@ -38,7 +38,7 @@ %p %strong Step 4. Push the result of the merge to GitLab - = clipboard_button(clipboard_target: "pre#merge-info-4", title: "Copy commands to clipboard") + = clipboard_button(target: "pre#merge-info-4", title: "Copy commands to clipboard") %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index caf3bf54eef..a0f54bd28ec 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -7,7 +7,7 @@ - if can_remove_source_branch = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do = icon('trash-o') - Remove Source Branch + Remove source branch - if mr_can_be_reverted = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "close") - if mr_can_be_cherry_picked diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index e5ec151a61d..cb117d1908c 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -10,24 +10,24 @@ - if @pipeline && @pipeline.active? %span.btn-group = button_tag class: "btn btn-info js-merge-when-pipeline-succeeds-button merge-when-pipeline-succeeds" do - Merge When Pipeline Succeeds + Merge when pipeline succeeds - unless @project.only_allow_merge_if_pipeline_succeeds? = button_tag class: "btn btn-info dropdown-toggle", 'data-toggle' => 'dropdown' do = icon('caret-down') %span.sr-only - Select Merge Moment + Select merge moment %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } %li = link_to "#", class: "merge_when_pipeline_succeeds" do = icon('check fw') - Merge When Pipeline Succeeds + Merge when pipeline succeeds %li = link_to "#", class: "accept-merge-request" do = icon('warning fw') - Merge Immediately + Merge immediately - else = f.button class: "btn btn-grouped js-merge-button accept-merge-request" do - Accept Merge Request + Accept merge request - if @merge_request.force_remove_source_branch? .accept-control The source branch will be removed. diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml index 5f347acce4d..76cc1ecd8a5 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml @@ -26,8 +26,8 @@ - if remove_source_branch_button = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') - Remove Source Branch When Merged + Remove source branch when merged - if user_can_cancel_automatic_merge = link_to cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do - Cancel Automatic Merge + Cancel automatic merge diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index b6340a00b29..8e85b2e8a20 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -9,8 +9,8 @@ .nav-controls = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do - New Milestone + = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone' do + New milestone .milestones %ul.content-list diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 5249d752585..8b62b156853 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -23,9 +23,9 @@ .milestone-buttons - if can?(current_user, :admin_milestone, @project) - if @milestone.active? - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" + = link_to 'Close milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" - else - = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" + = link_to 'Reopen milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do Edit diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index e8e450742b5..8b4e5928e0d 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -9,6 +9,6 @@ .note-form-actions.clearfix .settings-message.note-edit-warning.js-edit-warning Finish editing this message first! - = submit_tag 'Save Comment', class: 'btn btn-nr btn-save js-comment-button' + = submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-button' %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } Cancel diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 6c0e6d48d6c..9130dc128fa 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -5,8 +5,11 @@ %li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} } .timeline-entry-inner .timeline-icon - %a{ href: user_path(note.author) } - = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' + - if note.system + = icon_for_system_note(note) + - else + %a{ href: user_path(note.author) } + = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header %a.visible-xs{ href: user_path(note.author) } @@ -59,7 +62,9 @@ - if note.emoji_awardable? = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = icon('spinner spin') - = icon('smile-o', class: 'link-highlight') + %span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') + %span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley') + %span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile') - if note_editable = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 4be9a1371ec..ab6baaf35b6 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = render 'ci/status/badge', status: @pipeline.detailed_status(current_user) + = render 'ci/status/badge', status: @pipeline.detailed_status(current_user), title: @pipeline.status_title %strong Pipeline ##{@pipeline.id} triggered #{time_ago_with_tooltip(@pipeline.created_at)} - if @pipeline.user @@ -46,4 +46,4 @@ \... %span.js-details-content.hide = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace commit-hash-full" - = clipboard_button(clipboard_text: @pipeline.sha, title: "Copy commit SHA to clipboard") + = clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard") diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 53067cdcba4..d7cefb8613e 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -36,7 +36,6 @@ %th Job ID %th Name %th - - if pipeline.project.build_coverage_enabled? - %th Coverage + %th Coverage %th = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 132f6372e40..a3f84476dea 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -21,7 +21,7 @@ Git strategy for pipelines %p Choose between <code>clone</code> or <code>fetch</code> to get the recent application code - = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy') + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' .radio = f.label :build_allow_git_fetch_false do = f.radio_button :build_allow_git_fetch, 'false' @@ -43,7 +43,7 @@ = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' %p.help-block Per job in minutes. If a job passes this threshold, it will be marked as failed. - = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout') + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' %hr .form-group @@ -53,7 +53,16 @@ %strong Public pipelines .help-block Allow everyone to access pipelines for public and internal projects - = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines') + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank' + %hr + .form-group + .checkbox + = f.label :auto_cancel_pending_pipelines do + = f.check_box :auto_cancel_pending_pipelines, {}, 'enabled', 'disabled' + %strong Auto-cancel redundant, pending pipelines + .help-block + New pipelines will cancel older, pending pipelines on the same branch + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank' %hr .form-group @@ -65,7 +74,7 @@ %p.help-block A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable - = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing') + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank' .bs-callout.bs-callout-info %p Below are examples of regex for existing tools: %ul diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml new file mode 100644 index 00000000000..d183ce34a3a --- /dev/null +++ b/app/views/projects/registry/repositories/_image.html.haml @@ -0,0 +1,32 @@ +.container-image.js-toggle-container + .container-image-head + = link_to "#", class: "js-toggle-button" do + = icon('chevron-down', 'aria-hidden': 'true') + = escape_once(image.path) + + = clipboard_button(clipboard_text: "docker pull #{image.path}") + + .controls.hidden-xs.pull-right + = link_to namespace_project_container_registry_path(@project.namespace, @project, image), + class: 'btn btn-remove has-tooltip', + title: 'Remove repository', + data: { confirm: 'Are you sure?' }, + method: :delete do + = icon('trash cred', 'aria-hidden': 'true') + + .container-image-tags.js-toggle-content.hide + - if image.has_tags? + .table-holder + %table.table.tags + %thead + %tr + %th Tag + %th Tag ID + %th Size + %th Created + - if can?(current_user, :update_container_image, @project) + %th + = render partial: 'tag', collection: image.tags + - else + .nothing-here-block No tags in Container Registry for this container image. + diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/registry/repositories/_tag.html.haml index 10822b6184c..854b7d0ebf7 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/registry/repositories/_tag.html.haml @@ -1,7 +1,7 @@ %tr.tag %td = escape_once(tag.name) - = clipboard_button(clipboard_text: "docker pull #{tag.path}") + = clipboard_button(text: "docker pull #{tag.path}") %td - if tag.revision %span.has-tooltip{ title: "#{tag.revision}" } @@ -25,5 +25,9 @@ - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do - = icon("trash cred") + = link_to namespace_project_registry_repository_tag_path(@project.namespace, @project, tag.repository, tag.name), + method: :delete, + class: 'btn btn-remove has-tooltip', + title: 'Remove tag', + data: { confirm: 'Are you sure you want to delete this tag?' } do + = icon('trash cred') diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 993da27310f..be128e92fa7 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -15,25 +15,12 @@ %br Then you are free to create and upload a container image with build and push commands: %pre - docker build -t #{escape_once(@project.container_registry_repository_url)} . + docker build -t #{escape_once(@project.container_registry_url)}/image . %br - docker push #{escape_once(@project.container_registry_repository_url)} + docker push #{escape_once(@project.container_registry_url)}/image - - if @tags.blank? - %li - .nothing-here-block No images in Container Registry for this project. + - if @images.blank? + .nothing-here-block No container image repositories in Container Registry for this project. - else - .table-holder - %table.table.tags - %thead - %tr - %th Name - %th Image ID - %th Size - %th Created - - if can?(current_user, :update_container_image, @project) - %th - - - @tags.each do |tag| - = render 'tag', tag: tag + = render partial: 'image', collection: @images diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 2fb88297fb3..ef3599460f1 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -22,14 +22,14 @@ .col-sm-10.col-xs-12.input-group = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#display_name') + = clipboard_button(target: '#display_name') .form-group = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.input-group = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#description') + = clipboard_button(target: '#description') .form-group = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' @@ -46,7 +46,7 @@ .col-sm-10.col-xs-12.input-group = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#request_url') + = clipboard_button(target: '#request_url') .form-group = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' @@ -57,14 +57,14 @@ .col-sm-10.col-xs-12.input-group = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#response_username') + = clipboard_button(target: '#response_username') .form-group = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.input-group = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#response_icon') + = clipboard_button(target: '#response_icon') .form-group = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' @@ -75,14 +75,14 @@ .col-sm-10.col-xs-12.input-group = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_hint') + = clipboard_button(target: '#autocomplete_hint') .form-group = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.input-group = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_description') + = clipboard_button(target: '#autocomplete_description') %hr diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 078b7be6865..73b99453a4b 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -40,7 +40,7 @@ .col-sm-10.col-xs-12.input-group = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#url') + = clipboard_button(target: '#url') .form-group = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label' @@ -51,7 +51,7 @@ .col-sm-10.col-xs-12.input-group = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#customize_name') + = clipboard_button(target: '#customize_name') .form-group = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label' @@ -68,21 +68,21 @@ .col-sm-10.col-xs-12.input-group = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_description') + = clipboard_button(target: '#autocomplete_description') .form-group = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.input-group = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_usage_hint') + = clipboard_button(target: '#autocomplete_usage_hint') .form-group = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.input-group = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly' .input-group-btn - = clipboard_button(clipboard_target: '#descriptive_label') + = clipboard_button(target: '#descriptive_label') %hr diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml index 28e1c060875..f93994bebe3 100644 --- a/app/views/projects/stage/_stage.html.haml +++ b/app/views/projects/stage/_stage.html.haml @@ -6,8 +6,8 @@ = ci_icon_for_status(stage.status) = stage.name.titleize -= render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true -= render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true += render stage.statuses.latest_ordered, stage: false, ref: false, pipeline_link: false, allow_retry: true += render stage.statuses.retried_ordered, stage: false, ref: false, pipeline_link: false, retried: true %tr %td{ colspan: 10 } diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 6855c463c6d..2497a2d91b1 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -10,7 +10,7 @@ %i.fa.fa-angle-right %small.light = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" - = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard") + = clipboard_button(text: @commit.id, title: "Copy commit SHA to clipboard") = time_ago_with_tooltip(@commit.committed_date) \- = @commit.full_title diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index ed68e0ed56d..9b5f63ae81a 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -2,7 +2,7 @@ %td - if can?(current_user, :admin_trigger, trigger) %span= trigger.token - = clipboard_button(clipboard_text: trigger.token, title: "Copy trigger token to clipboard") + = clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard") - else %span= trigger.short_token diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 5211ade1a5f..86178257af8 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,9 +1,9 @@ - if (@page && @page.persisted?) - if can?(current_user, :create_wiki, @project) = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New Page + New page = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do - Page History + Page history - if can?(current_user, :create_wiki, @project) && @page.latest? = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do Edit diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 3d33679f07d..ba47574563d 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -18,4 +18,4 @@ Tip: You can specify the full path for the new file. We will automatically create any missing directories. .form-actions - = button_tag 'Create Page', class: 'build-new-wiki btn btn-create' + = button_tag 'Create page', class: 'build-new-wiki btn btn-create' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 8cf018da1b7..b995d08cd02 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -22,10 +22,10 @@ .nav-controls - if can?(current_user, :create_wiki, @project) = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New Page + New page - if @page.persisted? = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do - Page History + Page history - if can?(current_user, :admin_wiki, @project) = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do Delete diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 03684389742..34a4d7398bc 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -19,7 +19,7 @@ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true .input-group-btn - = clipboard_button(clipboard_target: '#project_clone', title: "Copy URL to clipboard") + = clipboard_button(target: '#project_clone', title: "Copy URL to clipboard") :javascript $('ul.clone-options-dropdown a').on('click',function(e){ diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml index af4cc90f4a7..e8062848fc3 100644 --- a/app/views/shared/_personal_access_tokens_form.html.haml +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -1,4 +1,4 @@ -- type = impersonation ? "Impersonation" : "Personal Access" +- type = impersonation ? "impersonation" : "personal access" %h5.prepend-top-0 Add a #{type} Token @@ -22,7 +22,7 @@ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes .prepend-top-default - = f.submit "Create #{type} Token", class: "btn btn-create" + = f.submit "Create #{type} token", class: "btn btn-create" :javascript var $dateField = $('.datepicker'); diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml index 67a49815478..ab7a2db002e 100644 --- a/app/views/shared/_personal_access_tokens_table.html.haml +++ b/app/views/shared/_personal_access_tokens_table.html.haml @@ -33,7 +33,7 @@ - if impersonation %td.token-token-container = text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control" - = clipboard_button(clipboard_text: token.token) + = clipboard_button(text: token.token) - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token) %td= link_to "Revoke", path, method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } - else diff --git a/app/views/shared/empty_states/monitoring/_getting_started.svg b/app/views/shared/empty_states/monitoring/_getting_started.svg new file mode 100644 index 00000000000..db7a1c2e708 --- /dev/null +++ b/app/views/shared/empty_states/monitoring/_getting_started.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="0" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="2" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="4" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="1" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="5" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="matrix(.99619.08716-.08716.99619 19.08-16.813)" rx="10"/><g transform="matrix(.96593.25882-.25882.96593 227.1 57.47)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g transform="translate(24.368 36.951)"><path fill="#d2caea" fill-rule="nonzero" d="m71.785 44.2c.761.296 1.625.099 2.184-.496l35.956-38.34c.756-.806.715-2.071-.091-2.827-.806-.756-2.071-.715-2.827.091l-35.03 37.36-41.888-16.285c-.749-.291-1.6-.106-2.16.471l-26.368 27.16c-.769.793-.751 2.059.042 2.828.793.769 2.059.751 2.828-.042l25.444-26.21 41.911 16.294"/><g fill="#fff"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="matrix(.99619-.08716.08716.99619-12.703 10.717)" rx="10"/><g transform="matrix(.99619.08716-.08716.99619 126.61 137.8)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="m84.67 28.41c18.225 0 33 15.07 33 33.651h-33v-33.651" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="m78.67 66.41h30c1.105 0 2 .895 2 2 0 18.778-15.222 34-34 34-18.778 0-34-15.222-34-34 0-18.778 15.222-34 34-34 1.105 0 2 .895 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28h-29.934c-1.105 0-2-.895-2-2v-29.934c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="matrix(.99619-.08716.08716.99619 30 88.03)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g transform="translate(42 34)"><path fill="#fef0ea" d="m0 13.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391v49.609h-12v-49.609"/><path fill="#fb722e" d="m66 21.406c0-.777.628-1.406 1.4-1.406h9.2c.773 0 1.4.624 1.4 1.406v41.594h-12v-41.594"/><path fill="#6b4fbb" d="m22 1.404c0-.776.628-1.404 1.4-1.404h9.2c.773 0 1.4.624 1.4 1.404v61.6h-12v-61.6"/><path fill="#d2caea" d="m44 39.4c0-.772.628-1.398 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602h-12v-23.602"/></g></g><g fill="#fee8dc"><path d="m6.226 94.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" transform="matrix(.70711.70711-.70711.70711 66.33 22.317)"/><path d="m312.78 53.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 126.1-206.88)"/></g><path fill="#e1dcf1" d="m124.78 12.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711 31.05 90.51)"/><path fill="#d2caea" d="m374.78 244.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711-59.779 335.24)"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/monitoring/_loading.svg b/app/views/shared/empty_states/monitoring/_loading.svg new file mode 100644 index 00000000000..6bbd7a6c5b9 --- /dev/null +++ b/app/views/shared/empty_states/monitoring/_loading.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="C" width="161" height="100" x="92" y="181" rx="10"/><rect id="E" width="151" height="32" x="20" rx="10"/><rect id="G" width="191" height="62" y="10" rx="10"/><circle id="I" cx="23" cy="41" r="9"/><circle id="4" cx="36.5" cy="36.5" r="36.5"/><circle id="8" cx="262.5" cy="169.5" r="15.5"/><circle id="A" cx="79.5" cy="169.5" r="15.5"/><circle id="K" cx="45" cy="41" r="9"/><circle id="0" cx="30.5" cy="30.5" r="30.5"/><circle id="2" cx="18" cy="34" r="3"/><ellipse id="6" cx="43.5" cy="43.5" rx="43.5" ry="43.5"/><mask id="H" width="191" height="62" x="0" y="0" fill="#fff"><use xlink:href="#G"/></mask><mask id="J" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#I"/></mask><mask id="D" width="161" height="100" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="F" width="151" height="32" x="0" y="0" fill="#fff"><use xlink:href="#E"/></mask><mask id="9" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="1" width="61" height="61" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="B" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="3" width="6" height="6" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="7" width="87" height="87" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="L" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#K"/></mask><mask id="5" width="73" height="73" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(28 2)"><g transform="translate(133 87)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><path stroke="#d2caea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="m19 32l2-9 5 17 4-12 4 5 6-10 3 5"/><g fill="#fff" stroke="#fb722e"><use stroke-width="4" mask="url(#3)" xlink:href="#2"/><circle cx="44" cy="30" r="2" stroke-width="2"/></g></g><g transform="translate(188 29)"><circle cx="36.5" cy="41.5" r="36.5" fill="#f9f9f9"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><rect width="27" height="4" x="23" y="27" fill="#d2caea" rx="2"/><rect width="10.5" height="4" x="23" y="27" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="36" fill="#d2caea" rx="2"/><rect width="19" height="4" x="23" y="36" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="45" fill="#d2caea" rx="2"/><rect width="7" height="4" x="23" y="45" fill="#6b4fbb" rx="2"/></g><path fill="#eee" fill-rule="nonzero" d="m247 292v1c0 5.519-4.469 9.993-10.01 9.993h-125.99c-5.177 0-9.436-3.927-9.954-8.96 1.348.998 2.957 1.666 4.705 1.883 1.027 1.835 2.992 3.077 5.248 3.077h125.99c2.485 0 4.611-1.497 5.526-3.637 1.796-.675 3.347-1.852 4.48-3.359m1.947-8.962c-.518 5.03-4.774 8.958-9.95 8.958h-131.99c-4.929 0-9.03-3.563-9.851-8.25 1.382.767 2.964 1.216 4.649 1.248 1.037 1.794 2.978 3 5.202 3h131.99c2.255 0 4.219-1.241 5.245-3.076 1.748-.216 3.356-.883 4.705-1.882"/><g transform="translate(79)"><ellipse cx="43.5" cy="47.5" fill="#f9f9f9" rx="43.5" ry="43.5"/><g fill="#fff"><g stroke="#eee"><use stroke-width="8" mask="url(#7)" xlink:href="#6"/><path stroke-width="4" d="m18.595 49c2.515 11.44 12.71 20 24.905 20 14.08 0 25.5-11.417 25.5-25.5 0-12.195-8.56-22.391-20-24.905v15.959c3 1.848 5 5.164 5 8.946 0 5.799-4.701 10.5-10.5 10.5-3.782 0-7.098-2-8.946-5h-15.959" stroke-linejoin="round"/></g><path stroke="#d2caea" stroke-width="4" d="m18 44c-.003-.166-.005-.333-.005-.5 0-14.08 11.417-25.5 25.5-25.5.167 0 .334.002.5.005v15.01c-.166-.008-.332-.012-.5-.012-5.799 0-10.5 4.701-10.5 10.5 0 .168.004.334.012.5h-15.01" stroke-linejoin="round"/></g></g><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill="#eee"><rect width="15" height="2" x="226" y="247" rx="1"/><rect width="15" height="2" x="226" y="242" rx="1"/><rect width="15" height="2" x="226" y="252" rx="1"/></g><rect width="10" height="52" x="118" y="196" fill="#d2caea" rx="2"/><rect width="10" height="47" x="154" y="196" fill="#6b4fbb" rx="2"/><rect width="10" height="37" x="190" y="196" fill="#d2caea" rx="2"/><g fill="#fee8dc"><rect width="10" height="52" x="132" y="185" rx="2"/><rect width="10" height="38" x="168" y="185" rx="2"/></g><rect width="10" height="58" x="204" y="185" fill="#fb722e" rx="2"/><g transform="translate(76 128)"><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#F)" xlink:href="#E"/><use mask="url(#H)" xlink:href="#G"/></g><g fill="#d2caea"><rect width="16" height="4" x="156" y="35" rx="2"/><rect width="16" height="4" x="156" y="43" rx="2"/></g><g fill="#fff" stroke-width="8"><use stroke="#fee8dc" mask="url(#J)" xlink:href="#I"/><use stroke="#fb722e" mask="url(#L)" xlink:href="#K"/></g></g><g fill="#fb722e"><path d="m6.226 220.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 155.43 59.22)"/><path d="m256.23 9.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 79.45-179.36)"/></g><path fill="#fee8dc" d="m312.78 150.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 194.69-178.47)"/><path fill="#6b4fbb" d="m43.778 80.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" opacity=".2" transform="matrix(.70711-.70711.70711.70711-40.761 53.15)"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/monitoring/_unable_to_connect.svg b/app/views/shared/empty_states/monitoring/_unable_to_connect.svg new file mode 100644 index 00000000000..62537d87d5d --- /dev/null +++ b/app/views/shared/empty_states/monitoring/_unable_to_connect.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><use id="0" xlink:href="#E"/><use id="2" xlink:href="#E"/><use id="4" xlink:href="#E"/><path id="6" d="m74 93h26v47h-26z"/><path id="8" d="m74 93h26v47h-26z"/><rect id="A" width="65" height="14" x="55" y="135" rx="4"/><rect id="C" width="175" height="118" rx="10"/><rect id="E" width="159" rx="10" height="56"/><rect id="F" width="160" y="2" rx="10" height="56" fill="#f9f9f9"/><mask id="B" width="65" height="14" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="9" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="D" width="175" height="118" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="7" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="3" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="1" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="5" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(1 65)"><g transform="translate(244)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><g fill="#d2caea"><rect width="50" height="4" x="19" y="20" rx="2"/><rect width="50" height="4" x="19" y="34" rx="2"/></g><g transform="translate(0 59)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#fee8dc" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fb722e" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m100 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><rect width="50" height="4" x="19" y="19" fill="#d2caea" rx="2" id="G"/><rect width="50" height="4" x="19" y="33" fill="#d2caea" rx="2" id="H"/></g><g transform="translate(0 118)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><use xlink:href="#G"/><use xlink:href="#H"/></g></g><g transform="translate(163 55)"><g fill="#eee"><rect width="29" height="4" y="29" rx="2"/><rect width="28" height="4" x="55" y="29" rx="2"/></g><g transform="translate(16)"><circle cx="30" cy="30" r="24" fill="#fef0ea"/><g fill="#fb722e"><circle cx="30.5" cy="30.5" r="30.5" opacity=".1"/><circle cx="30.5" cy="30.5" r="19.5" opacity=".1"/></g><circle cx="30.5" cy="30.5" r="13.5" fill="#fff"/><path fill="#fb722e" d="m32.621 30.5l2.481-2.481c.586-.586.58-1.529-.006-2.115-.59-.59-1.533-.589-2.115-.006l-2.481 2.481-2.481-2.481c-.586-.586-1.529-.58-2.115.006-.59.59-.589 1.533-.006 2.115l2.481 2.481-2.481 2.481c-.586.586-.58 1.529.006 2.115.59.59 1.533.589 2.115.006l2.481-2.481 2.481 2.481c.586.586 1.529.58 2.115-.006.59-.59.589-1.533.006-2.115l-2.481-2.481"/></g></g><g transform="translate(0 13)"><rect width="65" height="14" x="55" y="137" fill="#f9f9f9" rx="4"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#7)" xlink:href="#6"/><rect width="175" height="118" y="3" fill="#f9f9f9" rx="10"/><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill-rule="nonzero"><path fill="#eee" d="m163 105v-93h-152v93h152m-156-93.01c0-2.204 1.797-3.99 3.995-3.99h152.01c2.206 0 3.995 1.796 3.995 3.99v93.02c0 2.204-1.797 3.99-3.995 3.99h-152.01c-2.206 0-3.995-1.796-3.995-3.99v-93.02"/><path fill="#d2caea" d="m86 92c-11.598 0-21-9.402-21-21 0-11.598 9.402-21 21-21 11.598 0 21 9.402 21 21 0 11.598-9.402 21-21 21m0-4c9.389 0 17-7.611 17-17 0-9.389-7.611-17-17-17-9.389 0-17 7.611-17 17 0 9.389 7.611 17 17 17"/></g><path fill="#6b4fbb" d="m83 63c0-1.659 1.347-3 3-3 1.657 0 3 1.342 3 3v7.993c0 1.659-1.347 3-3 3-1.657 0-3-1.342-3-3v-7.993m3 18.997c-1.657 0-3-1.343-3-3 0-1.657 1.343-3 3-3 1.657 0 3 1.343 3 3 0 1.657-1.343 3-3 3"/><g fill="#eee"><rect width="134" height="4" x="20" y="30" rx="2"/><rect width="14" height="4" x="20" y="20" rx="2"/><circle cx="87" cy="21" r="5"/></g></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_emoji_slightly_smiling_face.svg b/app/views/shared/icons/_emoji_slightly_smiling_face.svg new file mode 100644 index 00000000000..56dbad91554 --- /dev/null +++ b/app/views/shared/icons/_emoji_slightly_smiling_face.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445c.195.625.556 1.131 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg> diff --git a/app/views/shared/icons/_emoji_smile.svg b/app/views/shared/icons/_emoji_smile.svg new file mode 100644 index 00000000000..ce645fee46f --- /dev/null +++ b/app/views/shared/icons/_emoji_smile.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg> diff --git a/app/views/shared/icons/_emoji_smiley.svg b/app/views/shared/icons/_emoji_smiley.svg new file mode 100644 index 00000000000..ddfae50e566 --- /dev/null +++ b/app/views/shared/icons/_emoji_smiley.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="nonzero"/></svg> diff --git a/app/views/shared/icons/_icon_arrow_circle_o_right.svg b/app/views/shared/icons/_icon_arrow_circle_o_right.svg new file mode 100644 index 00000000000..db28b5e2d7a --- /dev/null +++ b/app/views/shared/icons/_icon_arrow_circle_o_right.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1280 896q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192h-352q-13 0-22.5-9.5t-9.5-22.5v-192q0-13 9.5-22.5t22.5-9.5h352v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23zm160 0q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg> diff --git a/app/views/shared/icons/_icon_check_square_o.svg b/app/views/shared/icons/_icon_check_square_o.svg new file mode 100644 index 00000000000..3dfbfc8c0e9 --- /dev/null +++ b/app/views/shared/icons/_icon_check_square_o.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1472 930v318q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-10 10-23 10-3 0-9-2-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-254q0-13 9-22l64-64q10-10 23-10 6 0 12 3 20 8 20 29zm231-489l-814 814q-24 24-57 24t-57-24l-430-430q-24-24-24-57t24-57l110-110q24-24 57-24t57 24l263 263 647-647q24-24 57-24t57 24l110 110q24 24 24 57t-24 57z"/></svg> diff --git a/app/views/shared/icons/_icon_clock_o.svg b/app/views/shared/icons/_icon_clock_o.svg new file mode 100644 index 00000000000..8ddce62614c --- /dev/null +++ b/app/views/shared/icons/_icon_clock_o.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg> diff --git a/app/views/shared/icons/_icon_code_fork.svg b/app/views/shared/icons/_icon_code_fork.svg new file mode 100644 index 00000000000..5a0df2eee19 --- /dev/null +++ b/app/views/shared/icons/_icon_code_fork.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M672 1472q0-40-28-68t-68-28-68 28-28 68 28 68 68 28 68-28 28-68zm0-1152q0-40-28-68t-68-28-68 28-28 68 28 68 68 28 68-28 28-68zm640 128q0-40-28-68t-68-28-68 28-28 68 28 68 68 28 68-28 28-68zm96 0q0 52-26 96.5t-70 69.5q-2 287-226 414-68 38-203 81-128 40-169.5 71t-41.5 100v26q44 25 70 69.5t26 96.5q0 80-56 136t-136 56-136-56-56-136q0-52 26-96.5t70-69.5v-820q-44-25-70-69.5t-26-96.5q0-80 56-136t136-56 136 56 56 136q0 52-26 96.5t-70 69.5v497q54-26 154-57 55-17 87.5-29.5t70.5-31 59-39.5 40.5-51 28-69.5 8.5-91.5q-44-25-70-69.5t-26-96.5q0-80 56-136t136-56 136 56 56 136z"/></svg> diff --git a/app/views/shared/icons/_icon_comment_o.svg b/app/views/shared/icons/_icon_comment_o.svg new file mode 100644 index 00000000000..b99bd5f42c8 --- /dev/null +++ b/app/views/shared/icons/_icon_comment_o.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M896 384q-204 0-381.5 69.5t-282 187.5-104.5 255q0 112 71.5 213.5t201.5 175.5l87 50-27 96q-24 91-70 172 152-63 275-171l43-38 57 6q69 8 130 8 204 0 381.5-69.5t282-187.5 104.5-255-104.5-255-282-187.5-381.5-69.5zm896 512q0 174-120 321.5t-326 233-450 85.5q-70 0-145-8-198 175-460 242-49 14-114 22h-5q-15 0-27-10.5t-16-27.5v-1q-3-4-.5-12t2-10 4.5-9.5l6-9 7-8.5 8-9q7-8 31-34.5t34.5-38 31-39.5 32.5-51 27-59 26-76q-157-89-247.5-220t-90.5-281q0-174 120-321.5t326-233 450-85.5 450 85.5 326 233 120 321.5z"/></svg> diff --git a/app/views/shared/icons/_icon_commit.svg b/app/views/shared/icons/_icon_commit.svg index 0e96035b7b7..7e9c0ded04e 100644 --- a/app/views/shared/icons/_icon_commit.svg +++ b/app/views/shared/icons/_icon_commit.svg @@ -1,3 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"> - <path fill="#8F8F8F" fill-rule="evenodd" d="M28.7769836,18 C27.8675252,13.9920226 24.2831748,11 20,11 C15.7168252,11 12.1324748,13.9920226 11.2230164,18 L4.0085302,18 C2.90195036,18 2,18.8954305 2,20 C2,21.1122704 2.8992496,22 4.0085302,22 L11.2230164,22 C12.1324748,26.0079774 15.7168252,29 20,29 C24.2831748,29 27.8675252,26.0079774 28.7769836,22 L35.9914698,22 C37.0980496,22 38,21.1045695 38,20 C38,18.8877296 37.1007504,18 35.9914698,18 L28.7769836,18 L28.7769836,18 Z M20,25 C22.7614237,25 25,22.7614237 25,20 C25,17.2385763 22.7614237,15 20,15 C17.2385763,15 15,17.2385763 15,20 C15,22.7614237 17.2385763,25 20,25 L20,25 Z"/> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 18" enable-background="new 0 0 36 18"><path d="m34 7h-7.2c-.9-4-4.5-7-8.8-7s-7.9 3-8.8 7h-7.2c-1.1 0-2 .9-2 2 0 1.1.9 2 2 2h7.2c.9 4 4.5 7 8.8 7s7.9-3 8.8-7h7.2c1.1 0 2-.9 2-2 0-1.1-.9-2-2-2m-16 7c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5"/></svg> diff --git a/app/views/shared/icons/_icon_edit.svg b/app/views/shared/icons/_icon_edit.svg new file mode 100644 index 00000000000..cd4e34147e1 --- /dev/null +++ b/app/views/shared/icons/_icon_edit.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M888 1184l116-116-152-152-116 116v56h96v96h56zm440-720q-16-16-33 1l-350 350q-17 17-1 33t33-1l350-350q17-17 1-33zm80 594v190q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-14 14-32 8-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-126q0-13 9-22l64-64q15-15 35-7t20 29zm-96-738l288 288-672 672h-288v-288zm444 132l-92 92-288-288 92-92q28-28 68-28t68 28l152 152q28 28 28 68t-28 68z"/></svg> diff --git a/app/views/shared/icons/_icon_eye.svg b/app/views/shared/icons/_icon_eye.svg new file mode 100644 index 00000000000..2e2ae67142f --- /dev/null +++ b/app/views/shared/icons/_icon_eye.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1664 960q-152-236-381-353 61 104 61 225 0 185-131.5 316.5t-316.5 131.5-316.5-131.5-131.5-316.5q0-121 61-225-229 117-381 353 133 205 333.5 326.5t434.5 121.5 434.5-121.5 333.5-326.5zm-720-384q0-20-14-34t-34-14q-125 0-214.5 89.5t-89.5 214.5q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm848 384q0 34-20 69-140 230-376.5 368.5t-499.5 138.5-499.5-139-376.5-368q-20-35-20-69t20-69q140-229 376.5-368t499.5-139 499.5 139 376.5 368q20 35 20 69z"/></svg> diff --git a/app/views/shared/icons/_icon_eye_slash.svg b/app/views/shared/icons/_icon_eye_slash.svg new file mode 100644 index 00000000000..a16c5dcb24b --- /dev/null +++ b/app/views/shared/icons/_icon_eye_slash.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M555 1335l78-141q-87-63-136-159t-49-203q0-121 61-225-229 117-381 353 167 258 427 375zm389-759q0-20-14-34t-34-14q-125 0-214.5 89.5t-89.5 214.5q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm363-191q0 7-1 9-105 188-315 566t-316 567l-49 89q-10 16-28 16-12 0-134-70-16-10-16-28 0-12 44-87-143-65-263.5-173t-208.5-245q-20-31-20-69t20-69q153-235 380-371t496-136q89 0 180 17l54-97q10-16 28-16 5 0 18 6t31 15.5 33 18.5 31.5 18.5 19.5 11.5q16 10 16 27zm37 447q0 139-79 253.5t-209 164.5l280-502q8 45 8 84zm448 128q0 35-20 69-39 64-109 145-150 172-347.5 267t-419.5 95l74-132q212-18 392.5-137t301.5-307q-115-179-282-294l63-112q95 64 182.5 153t144.5 184q20 34 20 69z"/></svg> diff --git a/app/views/shared/icons/_icon_merge.svg b/app/views/shared/icons/_icon_merge.svg new file mode 100644 index 00000000000..451ae12afbc --- /dev/null +++ b/app/views/shared/icons/_icon_merge.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> diff --git a/app/views/shared/icons/_icon_merged.svg b/app/views/shared/icons/_icon_merged.svg new file mode 100644 index 00000000000..d8f96558bea --- /dev/null +++ b/app/views/shared/icons/_icon_merged.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M9.427 6.523a.932.932 0 0 0-.808.489v-.01c-.49-.01-1.059-.172-1.46-.489-.35-.278-.7-.772-.882-1.17a.964.964 0 0 0 .35-.744.943.943 0 0 0-.934-.959c-.518 0-.933.432-.933.964 0 .35.191.662.467.825v3.147a.97.97 0 0 0-.467.825c0 .532.415.959.933.959a.943.943 0 0 0 .934-.96.965.965 0 0 0-.467-.824V6.844c.313.336.672.61 1.073.81.402.202.948.303 1.386.308v-.01c.168.293.467.49.808.49a.943.943 0 0 0 .933-.96.943.943 0 0 0-.933-.96z"/></svg> diff --git a/app/views/shared/icons/_icon_pencil.svg b/app/views/shared/icons/_icon_pencil.svg new file mode 100644 index 00000000000..a3b48404f87 --- /dev/null +++ b/app/views/shared/icons/_icon_pencil.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z"/></svg> diff --git a/app/views/shared/icons/_icon_random.svg b/app/views/shared/icons/_icon_random.svg new file mode 100644 index 00000000000..763bd2d3dd8 --- /dev/null +++ b/app/views/shared/icons/_icon_random.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M666 481q-60 92-137 273-22-45-37-72.5t-40.5-63.5-51-56.5-63-35-81.5-14.5h-224q-14 0-23-9t-9-23v-192q0-14 9-23t23-9h224q250 0 410 225zm1126 799q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192q-32 0-85 .5t-81 1-73-1-71-5-64-10.5-63-18.5-58-28.5-59-40-55-53.5-56-69.5q59-93 136-273 22 45 37 72.5t40.5 63.5 51 56.5 63 35 81.5 14.5h256v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23zm0-896q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192h-256q-48 0-87 15t-69 45-51 61.5-45 77.5q-32 62-78 171-29 66-49.5 111t-54 105-64 100-74 83-90 68.5-106.5 42-128 16.5h-224q-14 0-23-9t-9-23v-192q0-14 9-23t23-9h224q48 0 87-15t69-45 51-61.5 45-77.5q32-62 78-171 29-66 49.5-111t54-105 64-100 74-83 90-68.5 106.5-42 128-16.5h256v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23z"/></svg> diff --git a/app/views/shared/icons/_icon_status_closed.svg b/app/views/shared/icons/_icon_status_closed.svg new file mode 100644 index 00000000000..de448ee1194 --- /dev/null +++ b/app/views/shared/icons/_icon_status_closed.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><rect x="3.36" y="6.16" width="7.28" height="1.68" rx=".84"/></svg> diff --git a/app/views/shared/icons/_icon_status_open.svg b/app/views/shared/icons/_icon_status_open.svg new file mode 100644 index 00000000000..ed58d23c626 --- /dev/null +++ b/app/views/shared/icons/_icon_status_open.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></svg> diff --git a/app/views/shared/icons/_icon_tags.svg b/app/views/shared/icons/_icon_tags.svg new file mode 100644 index 00000000000..fc5acc89c5e --- /dev/null +++ b/app/views/shared/icons/_icon_tags.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M384 448q0-53-37.5-90.5t-90.5-37.5-90.5 37.5-37.5 90.5 37.5 90.5 90.5 37.5 90.5-37.5 37.5-90.5zm1067 576q0 53-37 90l-491 492q-39 37-91 37-53 0-90-37l-715-716q-38-37-64.5-101t-26.5-117v-416q0-52 38-90t90-38h416q53 0 117 26.5t102 64.5l715 714q37 39 37 91zm384 0q0 53-37 90l-491 492q-39 37-91 37-36 0-59-14t-53-45l470-470q37-37 37-90 0-52-37-91l-715-714q-38-38-102-64.5t-117-26.5h224q53 0 117 26.5t102 64.5l715 714q37 39 37 91z"/></svg> diff --git a/app/views/shared/icons/_icon_user.svg b/app/views/shared/icons/_icon_user.svg new file mode 100644 index 00000000000..9b8cd74d62b --- /dev/null +++ b/app/views/shared/icons/_icon_user.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1600 1405q0 120-73 189.5t-194 69.5h-874q-121 0-194-69.5t-73-189.5q0-53 3.5-103.5t14-109 26.5-108.5 43-97.5 62-81 85.5-53.5 111.5-20q9 0 42 21.5t74.5 48 108 48 133.5 21.5 133.5-21.5 108-48 74.5-48 42-21.5q61 0 111.5 20t85.5 53.5 62 81 43 97.5 26.5 108.5 14 109 3.5 103.5zm-320-893q0 159-112.5 271.5t-271.5 112.5-271.5-112.5-112.5-271.5 112.5-271.5 271.5-112.5 271.5 112.5 112.5 271.5z"/></svg> diff --git a/app/views/shared/icons/_trash_o.svg b/app/views/shared/icons/_trash_o.svg new file mode 100644 index 00000000000..0d7a91ab536 --- /dev/null +++ b/app/views/shared/icons/_trash_o.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M704 736v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23v-576q0-14 9-23t23-9h64q14 0 23 9t9 23zm256 0v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23v-576q0-14 9-23t23-9h64q14 0 23 9t9 23zm256 0v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23v-576q0-14 9-23t23-9h64q14 0 23 9t9 23zm128 724v-948h-896v948q0 22 7 40.5t14.5 27 10.5 8.5h832q3 0 10.5-8.5t14.5-27 7-40.5zm-672-1076h448l-48-117q-7-9-17-11h-317q-10 2-17 11zm928 32v64q0 14-9 23t-23 9h-96v948q0 83-47 143.5t-113 60.5h-832q-66 0-113-58.5t-47-141.5v-952h-96q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h309l70-167q15-37 54-63t79-26h320q40 0 79 26t54 63l70 167h309q14 0 23 9t9 23z"/></svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 847a86e2e68..c72268473ca 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -40,21 +40,21 @@ .issues_bulk_update.hide = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do .filter-item.inline - = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do + = dropdown_tag("Status", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do %ul %li %a{ href: "#", data: { id: "reopen" } } Open %li %a{ href: "#", data: {id: "close" } } Closed .filter-item.inline - = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + = dropdown_tag("Assignee", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } }) .filter-item.inline - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'issue-bulk-update-dropdown-toggle js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } .filter-item.inline - = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do + = dropdown_tag("Subscription", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do %ul %li %a{ href: "#", data: { id: "subscribe" } } Subscribe diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 4c3c81a2f56..b447996a8ab 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -10,85 +10,93 @@ .check-all-holder = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" - .issues-other-filters.filtered-search-container - .filtered-search-input-container - .scroll-container - %ul.tokens-container.list-unstyled - %li.input-token - %input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } - = icon('filter') - %button.clear-search.hidden{ type: 'button' } - = icon('times') - #js-dropdown-hint.dropdown-menu.hint-dropdown - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { action: 'submit' } } - %button.btn.btn-link - = icon('search') - %span - Press Enter or click to search - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link - -# Encapsulate static class name `{{icon}}` inside #{} to bypass - -# haml lint's ClassAttributeWithStaticValue - %i.fa{ class: "#{'{{icon}}'}" } - %span.js-filter-hint - {{hint}} - %span.js-filter-tag.dropdown-light-content - {{tag}} - #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } } - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link.dropdown-user - %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } - .dropdown-user-details + .issues-other-filters.filtered-search-wrapper + .filtered-search-box + = dropdown_tag(content_tag(:i, '', class: 'fa fa-history'), + options: { wrapper_class: "filtered-search-history-dropdown-wrapper", + toggle_class: "filtered-search-history-dropdown-toggle-button", + dropdown_class: "filtered-search-history-dropdown", + content_class: "filtered-search-history-dropdown-content", + title: "Recent searches" }) do + .js-filtered-search-history-dropdown + .filtered-search-box-input-container + .scroll-container + %ul.tokens-container.list-unstyled + %li.input-token + %input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } + = icon('filter') + %button.clear-search.hidden{ type: 'button' } + = icon('times') + #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { action: 'submit' } } + %button.btn.btn-link + = icon('search') %span - {{name}} - %span.dropdown-light-content - @{{username}} - #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } } - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link - No Assignee - %li.divider - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link.dropdown-user - %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } - .dropdown-user-details - %span - {{name}} - %span.dropdown-light-content - @{{username}} - #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } } - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link - No Milestone - %li.filter-dropdown-item{ data: { value: 'upcoming' } } - %button.btn.btn-link - Upcoming - %li.filter-dropdown-item{ 'data-value' => 'started' } - %button.btn.btn-link - Started - %li.divider - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link.js-data-value - {{title}} - #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label', type: 'array' } } - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link - No Label - %li.divider - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link - %span.dropdown-label-box{ style: 'background: {{color}}' } - %span.label-title.js-data-value + Press Enter or click to search + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %i.fa{ class: "#{'{{icon}}'}" } + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} + #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.dropdown-user + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} + #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } + %button.btn.btn-link + No Assignee + %li.divider + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.dropdown-user + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} + #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } + %button.btn.btn-link + No Milestone + %li.filter-dropdown-item{ data: { value: 'upcoming' } } + %button.btn.btn-link + Upcoming + %li.filter-dropdown-item{ 'data-value' => 'started' } + %button.btn.btn-link + Started + %li.divider + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value {{title}} + #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label', type: 'array' } } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } + %button.btn.btn-link + No Label + %li.divider + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link + %span.dropdown-label-box{ style: 'background: {{color}}' } + %span.label-title.js-data-value + {{title}} .filter-dropdown-container - if type == :boards - if can?(current_user, :admin_list, @project) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 92d2d93a732..2e0d6a129fb 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -160,13 +160,13 @@ - project_ref = cross_project_reference(@project, issuable) .block.project-reference .sidebar-collapsed-icon.dont-change-state - = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") + = clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left") .cross-project-reference.hide-collapsed %span Reference: %cite{ title: project_ref } = project_ref - = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") + = clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left") :javascript gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml index 647e05e5ff7..e8b04f56839 100644 --- a/app/views/shared/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -29,5 +29,5 @@ - if @label.persisted? = f.submit 'Save changes', class: 'btn btn-save js-save-button' - else - = f.submit 'Create Label', class: 'btn btn-create js-save-button' + = f.submit 'Create label', class: 'btn btn-create js-save-button' = link_to 'Cancel', back_path, class: 'btn btn-cancel' diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 2810f1377b2..ccc808ff43e 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -122,10 +122,10 @@ - if milestone_ref.present? .block.reference .sidebar-collapsed-icon.dont-change-state - = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left") + = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left") .cross-project-reference.hide-collapsed %span Reference: %cite{ title: milestone_ref } = milestone_ref - = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left") + = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left") diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 37e2a377a69..ee3be3c789a 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -89,7 +89,7 @@ = f.label :enable_ssl_verification do = f.check_box :enable_ssl_verification %strong Enable SSL verification - = f.submit "Add Webhook", class: "btn btn-create" + = f.submit "Add webhook", class: "btn btn-create" %hr %h5.prepend-top-default Webhooks (#{hooks.count}) diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index adc07bcba73..00788e77b6b 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -7,13 +7,13 @@ - if current_user.two_factor_otp_enabled? .row.append-bottom-10 .col-md-3 - %button#js-setup-u2f-device.btn.btn-info Setup New U2F Device + %button#js-setup-u2f-device.btn.btn-info Setup new U2F device .col-md-9 %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. - else .row.append-bottom-10 .col-md-3 - %button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup New U2F Device + %button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup new U2F device .col-md-9 %p.text-warning You need to register a two-factor authentication app before you can set up a U2F device. @@ -36,7 +36,7 @@ = text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: "Pick a name" .col-md-3 = hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" - = submit_tag "Register U2F Device", class: "btn btn-success" + = submit_tag "Register U2F device", class: "btn btn-success" :javascript var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f); diff --git a/app/workers/trigger_schedule_worker.rb b/app/workers/trigger_schedule_worker.rb new file mode 100644 index 00000000000..440c579b99d --- /dev/null +++ b/app/workers/trigger_schedule_worker.rb @@ -0,0 +1,18 @@ +class TriggerScheduleWorker + include Sidekiq::Worker + include CronjobQueue + + def perform + Ci::TriggerSchedule.where("next_run_at < ?", Time.now).find_each do |trigger_schedule| + begin + Ci::CreateTriggerRequestService.new.execute(trigger_schedule.project, + trigger_schedule.trigger, + trigger_schedule.ref) + rescue => e + Rails.logger.error "#{trigger_schedule.id}: Failed to trigger_schedule job: #{e.message}" + ensure + trigger_schedule.schedule_next_run! + end + end + end +end |