diff options
144 files changed, 2793 insertions, 528 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dea11bb9f61..b442e48a3d0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -430,6 +430,7 @@ gitlab:assets:compile: USE_DB: "false" SKIP_STORAGE_VALIDATION: "true" WEBPACK_REPORT: "true" + NO_COMPRESSION: "true" script: - yarn install --pure-lockfile --production --cache-folder .yarn-cache - bundle exec rake gitlab:assets:compile diff --git a/app/assets/images/i2p-step.svg b/app/assets/images/i2p-step.svg new file mode 100644 index 00000000000..8886092ed82 --- /dev/null +++ b/app/assets/images/i2p-step.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 120" enable-background="new 0 0 12 120"> + <path d="m12 6c0-3.309-2.691-6-6-6s-6 2.691-6 6c0 2.967 2.167 5.431 5 5.91v108.09h2v-108.09c2.833-.479 5-2.943 5-5.91m-6 4c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4"/> +</svg> diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 53b25da18e5..baa20d0c34a 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -393,6 +393,9 @@ import ShortcutsBlob from './shortcuts_blob'; case 'users:show': new UserCallout(); break; + case 'admin:conversational_development_index:show': + new UserCallout(); + break; case 'snippets:show': new LineHighlighter(); new BlobViewer(); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index b8a923cf619..401dec1a370 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -2,6 +2,7 @@ import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; import { glEmojiTag } from '~/behaviors/gl_emoji'; import glRegexp from '~/lib/utils/regexp'; +import AjaxCache from '~/lib/utils/ajax_cache'; function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); @@ -35,6 +36,7 @@ class GfmAutoComplete { // This triggers at.js again // Needed for slash commands with suffixes (ex: /label ~) $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); + $input.on('clear-commands-cache.atwho', () => this.clearCache()); }); } @@ -375,11 +377,14 @@ class GfmAutoComplete { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); } else { - $.getJSON(this.dataSources[GfmAutoComplete.atTypeMap[at]], (data) => { - this.loadData($input, at, data); - }).fail(() => { this.isLoadingData[at] = false; }); + AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) + .then((data) => { + this.loadData($input, at, data); + }) + .catch(() => { this.isLoadingData[at] = false; }); } } + loadData($input, at, data) { this.isLoadingData[at] = false; this.cachedData[at] = data; @@ -389,6 +394,10 @@ class GfmAutoComplete { return $input.trigger('keyup'); } + clearCache() { + this.cachedData = {}; + } + static isLoading(data) { let dataToInspect = data; if (data && data.length > 0) { diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 800bb9f1fe8..e14414d3f68 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -7,6 +7,7 @@ import Service from '../services/index'; import Store from '../stores'; import titleComponent from './title.vue'; import descriptionComponent from './description.vue'; +import editedComponent from './edited.vue'; import formComponent from './form.vue'; import '../../lib/utils/url_utility'; @@ -50,6 +51,21 @@ export default { required: false, default: '', }, + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, issuableTemplates: { type: Array, required: false, @@ -86,6 +102,9 @@ export default { titleText: this.initialTitleText, descriptionHtml: this.initialDescriptionHtml, descriptionText: this.initialDescriptionText, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, }); return { @@ -98,10 +117,14 @@ export default { formState() { return this.store.formState; }, + hasUpdated() { + return !!this.state.updatedAt; + }, }, components: { descriptionComponent, titleComponent, + editedComponent, formComponent, }, methods: { @@ -240,6 +263,11 @@ export default { :description-text="state.descriptionText" :updated-at="state.updatedAt" :task-status="state.taskStatus" /> + <edited-component + v-if="hasUpdated" + :updated-at="state.updatedAt" + :updated-by-name="state.updatedByName" + :updated-by-path="state.updatedByPath" /> </div> </div> </template> diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 3281ec6b172..5ae617356e0 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -16,11 +16,6 @@ type: String, required: true, }, - updatedAt: { - type: String, - required: false, - default: '', - }, taskStatus: { type: String, required: false, @@ -31,7 +26,6 @@ return { preAnimation: false, pulseAnimation: false, - timeAgoEl: $('.js-issue-edited-ago'), }; }, watch: { @@ -39,12 +33,6 @@ this.animateChange(); this.$nextTick(() => { - const toolTipTime = gl.utils.formatDate(this.updatedAt); - - this.timeAgoEl.attr('datetime', this.updatedAt) - .attr('title', toolTipTime) - .tooltip('fixTitle'); - this.renderGFM(); }); }, diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue new file mode 100644 index 00000000000..d59e6d11032 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -0,0 +1,56 @@ +<script> +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + +export default { + props: { + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, + }, + components: { + timeAgoTooltip, + }, + computed: { + hasUpdatedBy() { + return this.updatedByName && this.updatedByPath; + }, + }, +}; +</script> + +<template> + <small + class="edited-text" + > + Edited + <time-ago-tooltip + v-if="updatedAt" + placement="bottom" + :time="updatedAt" + /> + <span + v-if="hasUpdatedBy" + > + by + <a + class="author_link" + :href="updatedByPath" + > + <span>{{updatedByName}}</span> + </a> + </span> + </small> +</template> + diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index faf79471946..14b2a1e18e9 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -42,6 +42,9 @@ document.addEventListener('DOMContentLoaded', () => { projectPath: this.projectPath, projectNamespace: this.projectNamespace, projectsAutocompleteUrl: this.projectsAutocompleteUrl, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, }, }); }, diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 4a16c3cb4dc..27c2d349f52 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -4,6 +4,9 @@ export default class Store { titleText, descriptionHtml, descriptionText, + updatedAt, + updatedByName, + updatedByPath, }) { this.state = { titleHtml, @@ -11,7 +14,9 @@ export default class Store { descriptionHtml, descriptionText, taskStatus: '', - updatedAt: '', + updatedAt, + updatedByName, + updatedByPath, }; this.formState = { title: '', @@ -30,6 +35,8 @@ export default class Store { this.state.descriptionText = data.description_text; this.state.taskStatus = data.task_status; this.state.updatedAt = data.updated_at; + this.state.updatedByName = data.updated_by_name; + this.state.updatedByPath = data.updated_by_path; } stateShouldUpdate(data) { diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index f1fe95e12e8..7477b5a5214 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -6,8 +6,8 @@ class AjaxCache extends Cache { this.pendingRequests = { }; } - retrieve(endpoint) { - if (this.hasData(endpoint)) { + retrieve(endpoint, forceRetrieve) { + if (this.hasData(endpoint) && !forceRetrieve) { return Promise.resolve(this.get(endpoint)); } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 0ca7cabfc5a..929965de5c1 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,6 +16,7 @@ import autosize from 'vendor/autosize'; import Dropzone from 'dropzone'; import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; +import AjaxCache from '~/lib/utils/ajax_cache'; import CommentTypeToggle from './comment_type_toggle'; import './autosave'; import './dropzone_input'; @@ -66,7 +67,6 @@ const normalizeNewlines = function(str) { this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); this.basePollingInterval = 15000; this.maxPollingSteps = 4; - this.flashErrors = []; this.cleanBinding(); this.addBinding(); @@ -325,6 +325,9 @@ const normalizeNewlines = function(str) { if (Notes.isNewNote(noteEntity, this.note_ids)) { this.note_ids.push(noteEntity.id); + if ($notesList.length) { + $notesList.find('.system-note.being-posted').remove(); + } const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList); this.setupNewNote($newNote); @@ -1118,12 +1121,14 @@ const normalizeNewlines = function(str) { }; Notes.prototype.addFlash = function(...flashParams) { - this.flashErrors.push(new Flash(...flashParams)); + this.flashInstance = new Flash(...flashParams); }; Notes.prototype.clearFlash = function() { - this.flashErrors.forEach(flash => flash.flashContainer.remove()); - this.flashErrors = []; + if (this.flashInstance && this.flashInstance.flashContainer) { + this.flashInstance.flashContainer.hide(); + this.flashInstance = null; + } }; Notes.prototype.cleanForm = function($form) { @@ -1187,7 +1192,7 @@ const normalizeNewlines = function(str) { Notes.prototype.getFormData = function($form) { return { formData: $form.serialize(), - formContent: $form.find('.js-note-text').val(), + formContent: _.escape($form.find('.js-note-text').val()), formAction: $form.attr('action'), }; }; @@ -1207,19 +1212,46 @@ const normalizeNewlines = function(str) { }; /** + * Gets appropriate description from slash commands found in provided `formContent` + */ + Notes.prototype.getSlashCommandDescription = function (formContent, availableSlashCommands = []) { + let tempFormContent; + + // Identify executed slash commands from `formContent` + const executedCommands = availableSlashCommands.filter((command, index) => { + const commandRegex = new RegExp(`/${command.name}`); + return commandRegex.test(formContent); + }); + + if (executedCommands && executedCommands.length) { + if (executedCommands.length > 1) { + tempFormContent = 'Applying multiple commands'; + } else { + const commandDescription = executedCommands[0].description.toLowerCase(); + tempFormContent = `Applying command to ${commandDescription}`; + } + } else { + tempFormContent = 'Applying command'; + } + + return tempFormContent; + }; + + /** * Create placeholder note DOM element populated with comment body * that we will show while comment is being posted. * Once comment is _actually_ posted on server, we will have final element * in response that we will show in place of this temporary element. */ - Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) { + Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { const discussionClass = isDiscussionNote ? 'discussion' : ''; - const escapedFormContent = _.escape(formContent); const $tempNote = $( `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> - <a href="/${currentUsername}"><span class="avatar dummy-avatar"></span></a> + <a href="/${currentUsername}"> + <img class="avatar s40" src="${currentUserAvatar}"> + </a> </div> <div class="timeline-content ${discussionClass}"> <div class="note-header"> @@ -1232,7 +1264,7 @@ const normalizeNewlines = function(str) { </div> <div class="note-body"> <div class="note-text"> - <p>${escapedFormContent}</p> + <p>${formContent}</p> </div> </div> </div> @@ -1244,6 +1276,23 @@ const normalizeNewlines = function(str) { }; /** + * Create Placeholder System Note DOM element populated with slash command description + */ + Notes.prototype.createPlaceholderSystemNote = function ({ formContent, uniqueId }) { + const $tempNote = $( + `<li id="${uniqueId}" class="note system-note timeline-entry being-posted fade-in-half"> + <div class="timeline-entry-inner"> + <div class="timeline-content"> + <i>${formContent}</i> + </div> + </div> + </li>` + ); + + return $tempNote; + }; + + /** * This method does following tasks step-by-step whenever a new comment * is submitted by user (both main thread comments as well as discussion comments). * @@ -1274,7 +1323,9 @@ const normalizeNewlines = function(str) { const isDiscussionForm = $form.hasClass('js-discussion-note-form'); const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); const { formData, formContent, formAction } = this.getFormData($form); - const uniqueId = _.uniqueId('tempNote_'); + let noteUniqueId; + let systemNoteUniqueId; + let hasSlashCommands = false; let $notesContainer; let tempFormContent; @@ -1295,16 +1346,28 @@ const normalizeNewlines = function(str) { tempFormContent = formContent; if (this.hasSlashCommands(formContent)) { tempFormContent = this.stripSlashCommands(formContent); + hasSlashCommands = true; } + // Show placeholder note if (tempFormContent) { - // Show placeholder note + noteUniqueId = _.uniqueId('tempNote_'); $notesContainer.append(this.createPlaceholderNote({ formContent: tempFormContent, - uniqueId, + uniqueId: noteUniqueId, isDiscussionNote, currentUsername: gon.current_username, currentUserFullname: gon.current_user_fullname, + currentUserAvatar: gon.current_user_avatar_url, + })); + } + + // Show placeholder system note + if (hasSlashCommands) { + systemNoteUniqueId = _.uniqueId('tempSystemNote_'); + $notesContainer.append(this.createPlaceholderSystemNote({ + formContent: this.getSlashCommandDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), + uniqueId: systemNoteUniqueId, })); } @@ -1322,7 +1385,13 @@ const normalizeNewlines = function(str) { gl.utils.ajaxPost(formAction, formData) .then((note) => { // Submission successful! remove placeholder - $notesContainer.find(`#${uniqueId}`).remove(); + $notesContainer.find(`#${noteUniqueId}`).remove(); + + // Reset cached commands list when command is applied + if (hasSlashCommands) { + $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); + } + // Clear previous form errors this.clearFlashWrapper(); @@ -1359,7 +1428,11 @@ const normalizeNewlines = function(str) { $form.trigger('ajax:success', [note]); }).fail(() => { // Submission failed, remove placeholder note and show Flash error message - $notesContainer.find(`#${uniqueId}`).remove(); + $notesContainer.find(`#${noteUniqueId}`).remove(); + + if (hasSlashCommands) { + $notesContainer.find(`#${systemNoteUniqueId}`).remove(); + } // Show form again on UI on failure if (isDiscussionForm && $notesContainer.length) { diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index b9d57cbcad4..ff2208baeab 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,11 +1,10 @@ import Cookies from 'js-cookie'; -const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; - export default class UserCallout { - constructor() { - this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); - this.userCalloutBody = $('.user-callout'); + constructor(className = 'user-callout') { + this.userCalloutBody = $(`.${className}`); + this.cookieName = this.userCalloutBody.data('uid'); + this.isCalloutDismissed = Cookies.get(this.cookieName); this.init(); } @@ -18,7 +17,7 @@ export default class UserCallout { dismissCallout(e) { const $currentTarget = $(e.currentTarget); - Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 }); + Cookies.set(this.cookieName, 'true', { expires: 365 }); if ($currentTarget.hasClass('close')) { this.userCalloutBody.remove(); diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 678af978edd..0140dcf19c3 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -112,11 +112,6 @@ } } - .issue-edited-ago, - .note_edited_ago { - display: none; - } - aside:not(.right-sidebar) { display: none; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 975a4b40383..b3a86b92d93 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -569,3 +569,10 @@ $filter-value-selected-color: #d7d7d7; Animation Functions */ $dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1); + +/* +Convdev Index +*/ +$color-high-score: $green-400; +$color-average-score: $orange-400; +$color-low-score: $red-400; diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss new file mode 100644 index 00000000000..0413114c279 --- /dev/null +++ b/app/assets/stylesheets/pages/convdev_index.scss @@ -0,0 +1,255 @@ +$space-between-cards: 8px; + +.convdev-empty svg { + margin: 64px auto 32px; + max-width: 420px; +} + +.convdev-header { + margin-top: $gl-padding; + margin-bottom: $gl-padding; + padding: 0 4px; + display: flex; + align-items: center; + + .convdev-header-title { + font-size: 48px; + line-height: 1; + margin: 0; + } + + .convdev-header-subtitle { + font-size: 22px; + line-height: 1; + color: $gl-text-color-secondary; + margin-left: 8px; + font-weight: 500; + + a { + font-size: 18px; + color: $gl-text-color-secondary; + + &:hover { + color: $blue-500; + } + } + } +} + +.convdev-cards { + display: flex; + justify-content: center; + flex-wrap: wrap; +} + +.convdev-card-wrapper { + display: flex; + flex-direction: column; + align-items: stretch; + text-align: center; + width: 50%; + border-color: $border-color; + margin: 0 0 32px; + padding: $space-between-cards / 2; + position: relative; + + @media (min-width: $screen-xs-min) { + width: percentage(1 / 4); + } + + @media (min-width: $screen-sm-min) { + width: percentage(1 / 5); + } + + @media (min-width: $screen-md-min) { + width: percentage(1 / 6); + } + + @media (min-width: $screen-lg-min) { + width: percentage(1 / 10); + } +} + +.convdev-card { + border: solid 1px $border-color; + border-radius: 3px; + border-top-width: 3px; + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.convdev-card-low { + border-top-color: $color-low-score; + + .card-score-big { + background-color: $red-25; + } +} + +.convdev-card-average { + border-top-color: $color-average-score; + + .card-score-big { + background-color: $orange-25; + } +} + +.convdev-card-high { + border-top-color: $color-high-score; + + .card-score-big { + background-color: $green-25; + } +} + +.convdev-card-title { + margin: $gl-padding auto auto; + max-width: 100px; + + h3 { + font-size: 14px; + margin: 0 0 2px; + } + + .text-light { + font-size: 13px; + line-height: 1.25; + color: $gl-text-color-secondary; + } +} + +.card-scores { + display: flex; + justify-content: space-around; + align-items: center; + margin: $gl-padding $gl-btn-padding; + line-height: 1; +} + +.card-score { + color: $gl-text-color-secondary; + + .card-score-name { + font-size: 13px; + margin-top: 4px; + } +} + +.card-score-value { + font-size: 16px; + color: $gl-text-color; + font-weight: 500; +} + +.card-score-big { + border-top: 2px solid $border-color; + border-bottom: 1px solid $border-color; + font-size: 22px; + padding: 10px 0; + font-weight: 500; +} + +.card-buttons { + display: flex; + + > * { + font-size: 16px; + color: $gl-text-color-secondary; + padding: 10px; + flex-grow: 1; + + &:hover { + background-color: $border-color; + color: $gl-text-color; + } + + + * { + border-left: solid 1px $border-color; + } + } +} + +.convdev-steps { + margin-top: $gl-padding; + height: 1px; + min-width: 100%; + justify-content: space-around; + position: relative; + background: $border-color; +} + +.convdev-step { + $step-positions: 5% 10% 30% 42% 48% 55% 60% 70% 75% 90%; + @each $pos in $step-positions { + $i: index($step-positions, $pos); + + &:nth-child(#{$i}) { + left: $pos; + } + } + + position: absolute; + transform-origin: 75% 50%; + padding: 8px; + height: 50px; + width: 50px; + border-radius: 3px; + display: flex; + flex-direction: column; + align-items: center; + border: solid 1px $border-color; + background: $white-light; + transform: translate(-50%, -50%); + color: $gl-text-color-secondary; + fill: $gl-text-color-secondary; + box-shadow: 0 2px 4px $dropdown-shadow-color; + + &:hover { + padding: 8px 10px; + fill: currentColor; + z-index: 100; + height: auto; + width: auto; + + .convdev-step-title { + max-height: 2em; + opacity: 1; + transition: opacity 0.2s; + } + + svg { + transform: scale(1.5); + margin: $gl-btn-padding; + } + } + + svg { + transition: transform 0.1s; + width: 30px; + height: 30px; + min-height: 30px; + min-width: 30px; + } +} + +.convdev-step-title { + max-height: 0; + opacity: 0; + text-transform: uppercase; + margin: $gl-vert-padding 0 0; + text-align: center; + font-size: 12px; +} + +.convdev-high-score { + color: $color-high-score; +} + +.convdev-average-score { + color: $color-average-score; +} + +.convdev-low-score { + color: $color-low-score; +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index fe084eb9397..c207159f606 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -287,6 +287,7 @@ table.u2f-registrations { .user-callout { margin: 0 auto; + max-width: $screen-lg-min; .bordered-box { border: 1px solid $blue-300; @@ -295,14 +296,15 @@ table.u2f-registrations { position: relative; display: flex; justify-content: center; + align-items: center; } .landing { - margin-top: $gl-padding; - margin-bottom: $gl-padding; + padding: 32px; .close { position: absolute; + top: 20px; right: 20px; opacity: 1; @@ -330,11 +332,20 @@ table.u2f-registrations { height: 110px; vertical-align: top; } + + &.convdev { + margin: 0 0 0 30px; + + svg { + height: 127px; + } + } } .user-callout-copy { display: inline-block; vertical-align: top; + max-width: 570px; } } @@ -348,12 +359,20 @@ table.u2f-registrations { .landing { .svg-container, .user-callout-copy { - margin: 0; + margin: 0 auto; display: block; svg { height: 75px; } + + &.convdev { + margin: $gl-padding auto 0; + + svg { + height: 120px; + } + } } } } diff --git a/app/controllers/admin/conversational_development_index_controller.rb b/app/controllers/admin/conversational_development_index_controller.rb new file mode 100644 index 00000000000..921169d3e2b --- /dev/null +++ b/app/controllers/admin/conversational_development_index_controller.rb @@ -0,0 +1,5 @@ +class Admin::ConversationalDevelopmentIndexController < Admin::ApplicationController + def show + @metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present + end +end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 59df1e7b86a..8b1efd0c572 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -199,14 +199,21 @@ class Projects::IssuesController < Projects::ApplicationController def realtime_changes Gitlab::PollingInterval.set_header(response, interval: 3_000) - render json: { + response = { title: view_context.markdown_field(@issue, :title), title_text: @issue.title, description: view_context.markdown_field(@issue, :description), description_text: @issue.description, - task_status: @issue.task_status, - updated_at: @issue.updated_at + task_status: @issue.task_status } + + if @issue.is_edited? + response[:updated_at] = @issue.updated_at + response[:updated_by_name] = @issue.last_edited_by.name + response[:updated_by_path] = user_path(@issue.last_edited_by) + end + + render json: response end def create_merge_request diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index d2cd1cfdab8..cb4f46388fd 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -45,6 +45,17 @@ class Projects::JobsController < 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 + Gitlab::PollingInterval.set_header(response, interval: 10_000) + + render json: BuildSerializer + .new(project: @project, current_user: @current_user) + .represent(@build, {}, BuildDetailsEntity) + end + end end def trace diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 36d9090b3ae..71154da7ec5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -181,7 +181,7 @@ module ApplicationHelper end def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', exclude_author: false) - return if object.last_edited_at == object.created_at || object.last_edited_at.blank? + return unless object.is_edited? content_tag :small, class: 'edited-text' do output = content_tag(:span, 'Edited ') @@ -275,8 +275,8 @@ module ApplicationHelper 'active' if condition end - def show_user_callout? - cookies[:user_callout_dismissed].nil? + def show_callout?(name) + cookies[name] != 'true' end def linkedin_url(user) diff --git a/app/helpers/conversational_development_index_helper.rb b/app/helpers/conversational_development_index_helper.rb new file mode 100644 index 00000000000..1ff54415811 --- /dev/null +++ b/app/helpers/conversational_development_index_helper.rb @@ -0,0 +1,16 @@ +module ConversationalDevelopmentIndexHelper + def score_level(score) + if score < 33.33 + 'low' + elsif score < 66.66 + 'average' + else + 'high' + end + end + + def format_score(score) + precision = score < 1 ? 2 : 1 + number_with_precision(score, precision: precision) + end +end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index c380a10c82d..5e8f0849969 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -200,7 +200,7 @@ module IssuablesHelper end def issuable_initial_data(issuable) - { + data = { endpoint: namespace_project_issue_path(@project.namespace, @project, issuable), canUpdate: can?(current_user, :update_issue, issuable), canDestroy: can?(current_user, :destroy_issue, issuable), @@ -217,7 +217,23 @@ module IssuablesHelper initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionText: issuable.description - }.to_json + } + + data.merge!(updated_at_by(issuable)) + + data.to_json + end + + def updated_at_by(issuable) + return {} unless issuable.is_edited? + + { + updatedAt: issuable.updated_at.to_time.iso8601, + updatedBy: { + name: issuable.last_edited_by.name, + path: user_path(issuable.last_edited_by) + } + } end private diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index c0763a8a9c4..8e0a1e2ecdf 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -13,6 +13,17 @@ module SubmoduleHelper if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ namespace, project = $1, $2 + gitlab_hosts = [Gitlab.config.gitlab.url, + Gitlab.config.gitlab_shell.ssh_path_prefix] + + gitlab_hosts.each do |host| + if url.start_with?(host) + namespace, _, project = url.sub(host, '').rpartition('/') + break + end + end + + namespace.sub!(/\A\//, '') project.rstrip! project.sub!(/\.git\z/, '') diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 58dfdd87652..cec1ca89a6a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -47,8 +47,8 @@ module Ci before_destroy { unscoped_project } after_create :execute_hooks - after_save :update_project_statistics, if: :artifacts_size_changed? - after_destroy :update_project_statistics + after_commit :update_project_statistics_after_save, on: [:create, :update] + after_commit :update_project_statistics, on: :destroy class << self # This is needed for url_for to work, @@ -138,6 +138,17 @@ module Ci ExpandVariables.expand(environment, simple_variables) if environment end + def environment_url + return @environment_url if defined?(@environment_url) + + @environment_url = + if unexpanded_url = options&.dig(:environment, :url) + ExpandVariables.expand(unexpanded_url, simple_variables) + else + persisted_environment&.external_url + end + end + def has_environment? environment.present? end @@ -198,20 +209,23 @@ module Ci # All variables, including those dependent on other variables def variables - variables = simple_variables - variables += persisted_environment.predefined_variables if persisted_environment.present? - variables + simple_variables.concat(persisted_environment_variables) end def merge_request - merge_requests = MergeRequest.includes(:merge_request_diff) - .where(source_branch: ref, - source_project: pipeline.project) - .reorder(iid: :asc) - - merge_requests.find do |merge_request| - merge_request.commits_sha.include?(pipeline.sha) - end + return @merge_request if defined?(@merge_request) + + @merge_request ||= + begin + merge_requests = MergeRequest.includes(:merge_request_diff) + .where(source_branch: ref, + source_project: pipeline.project) + .reorder(iid: :desc) + + merge_requests.find do |merge_request| + merge_request.commits_sha.include?(pipeline.sha) + end + end end def repo_url @@ -335,7 +349,7 @@ module Ci end def has_expiring_artifacts? - artifacts_expire_at.present? + artifacts_expire_at.present? && artifacts_expire_at > Time.now end def keep_artifacts! @@ -462,6 +476,18 @@ module Ci variables.concat(legacy_variables) end + def persisted_environment_variables + return [] unless persisted_environment + + variables = persisted_environment.predefined_variables + + if url = environment_url + variables << { key: 'CI_ENVIRONMENT_URL', value: url, public: true } + end + + variables + end + def legacy_variables variables = [ { key: 'CI_BUILD_ID', value: id.to_s, public: true }, @@ -491,5 +517,11 @@ module Ci ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size]) end + + def update_project_statistics_after_save + if previous_changes.include?('artifacts_size') + update_project_statistics + end + end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index fe63728ea23..8b4ed49269d 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -18,7 +18,7 @@ class CommitStatus < ActiveRecord::Base validates :name, presence: true alias_attribute :author, :user - + scope :failed_but_allowed, -> do where(allow_failure: true, status: [:failed, :canceled]) end @@ -83,14 +83,15 @@ class CommitStatus < ActiveRecord::Base next if transition.loopback? commit_status.run_after_commit do - pipeline.try do |pipeline| + if pipeline if complete? || manual? PipelineProcessWorker.perform_async(pipeline.id) else PipelineUpdateWorker.perform_async(pipeline.id) end - ExpireJobCacheWorker.perform_async(commit_status.id) end + + ExpireJobCacheWorker.perform_async(commit_status.id) end end @@ -126,6 +127,11 @@ class CommitStatus < ActiveRecord::Base false end + # To be overriden when inherrited from + def retryable? + false + end + def stuck? false end diff --git a/app/models/concerns/editable.rb b/app/models/concerns/editable.rb new file mode 100644 index 00000000000..c62c7e1e936 --- /dev/null +++ b/app/models/concerns/editable.rb @@ -0,0 +1,7 @@ +module Editable + extend ActiveSupport::Concern + + def is_edited? + last_edited_at.present? && last_edited_at != created_at + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 075ec575f9d..ea10d004c9c 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -15,6 +15,7 @@ module Issuable include Taskable include TimeTrackable include Importable + include Editable # This object is used to gather issuable meta data for displaying # upvotes, downvotes, notes and closing merge requests count for issues and merge requests diff --git a/app/models/conversational_development_index/card.rb b/app/models/conversational_development_index/card.rb new file mode 100644 index 00000000000..e8f09dc9161 --- /dev/null +++ b/app/models/conversational_development_index/card.rb @@ -0,0 +1,26 @@ +module ConversationalDevelopmentIndex + class Card + attr_accessor :metric, :title, :description, :feature, :blog, :docs + + def initialize(metric:, title:, description:, feature:, blog:, docs: nil) + self.metric = metric + self.title = title + self.description = description + self.feature = feature + self.blog = blog + self.docs = docs + end + + def instance_score + metric.instance_score(feature) + end + + def leader_score + metric.leader_score(feature) + end + + def percentage_score + metric.percentage_score(feature) + end + end +end diff --git a/app/models/conversational_development_index/idea_to_production_step.rb b/app/models/conversational_development_index/idea_to_production_step.rb new file mode 100644 index 00000000000..6e1753c9f30 --- /dev/null +++ b/app/models/conversational_development_index/idea_to_production_step.rb @@ -0,0 +1,19 @@ +module ConversationalDevelopmentIndex + class IdeaToProductionStep + attr_accessor :metric, :title, :features + + def initialize(metric:, title:, features:) + self.metric = metric + self.title = title + self.features = features + end + + def percentage_score + sum = features.sum do |feature| + metric.percentage_score(feature) + end + + sum / features.size.to_f + end + end +end diff --git a/app/models/conversational_development_index/metric.rb b/app/models/conversational_development_index/metric.rb new file mode 100644 index 00000000000..f42f516f99a --- /dev/null +++ b/app/models/conversational_development_index/metric.rb @@ -0,0 +1,21 @@ +module ConversationalDevelopmentIndex + class Metric < ActiveRecord::Base + include Presentable + + self.table_name = 'conversational_development_index_metrics' + + def instance_score(feature) + self["instance_#{feature}"] + end + + def leader_score(feature) + self["leader_#{feature}"] + end + + def percentage_score(feature) + return 100 if leader_score(feature).zero? + + 100 * instance_score(feature) / leader_score(feature) + end + end +end diff --git a/app/models/group.rb b/app/models/group.rb index be944da5a67..5bb2cdc5eff 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -222,6 +222,16 @@ class Group < Namespace User.where(id: members_with_parents.select(:user_id)) end + def max_member_access_for_user(user) + return GroupMember::OWNER if user.admin? + + members_with_parents. + where(user_id: user). + reorder(access_level: :desc). + first&. + access_level || GroupMember::NO_ACCESS + end + def mattermost_team_params max_length = 59 diff --git a/app/models/key.rb b/app/models/key.rb index b7956052c3f..cb8f10f6d55 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -1,7 +1,6 @@ require 'digest/md5' class Key < ActiveRecord::Base - include AfterCommitQueue include Sortable LAST_USED_AT_REFRESH_TIME = 1.day.to_i @@ -25,10 +24,10 @@ class Key < ActiveRecord::Base delegate :name, :email, to: :user, prefix: true - after_create :add_to_shell - after_create :notify_user + after_commit :add_to_shell, on: :create + after_commit :notify_user, on: :create after_create :post_create_hook - after_destroy :remove_from_shell + after_commit :remove_from_shell, on: :destroy after_destroy :post_destroy_hook def key=(value) @@ -93,6 +92,6 @@ class Key < ActiveRecord::Base end def notify_user - run_after_commit { NotificationService.new.new_key(self) } + NotificationService.new.new_key(self) end end diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index 007eed5600a..b0625c52b62 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -6,8 +6,7 @@ class LfsObjectsProject < ActiveRecord::Base validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :project_id, presence: true - after_create :update_project_statistics - after_destroy :update_project_statistics + after_commit :update_project_statistics, on: [:create, :destroy] private diff --git a/app/models/member.rb b/app/models/member.rb index 7228e82e978..29f9d61e870 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -200,6 +200,10 @@ class Member < ActiveRecord::Base source_type end + def access_field + access_level + end + def invite? self.invite_token.present? end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 28e10bc6172..47040f95533 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -25,10 +25,6 @@ class GroupMember < Member source end - def access_field - access_level - end - # Because source_type is `Namespace`... def real_source_type 'Group' diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index b3a91feb091..c0e17f4bfc8 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -79,10 +79,6 @@ class ProjectMember < Member end end - def access_field - access_level - end - def project source end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index aebee06d560..b48d73dcae7 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -6,6 +6,7 @@ class Namespace < ActiveRecord::Base include Gitlab::ShellAdapter include Gitlab::CurrentSettings include Routable + include AfterCommitQueue # Prevent users from creating unreasonably deep level of nesting. # The number 20 was taken based on maximum nesting level of @@ -242,7 +243,9 @@ class Namespace < ActiveRecord::Base # Remove namespace directroy async with delay so # GitLab has time to remove all projects first - GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) + run_after_commit do + GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) + end end end diff --git a/app/models/note.rb b/app/models/note.rb index 832c68243fb..563af47f314 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -13,6 +13,7 @@ class Note < ActiveRecord::Base include AfterCommitQueue include ResolvableNote include IgnorableColumn + include Editable ignore_column :original_discussion_id diff --git a/app/models/project.rb b/app/models/project.rb index 446329557d5..d21ff274b6e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -471,7 +471,9 @@ class Project < ActiveRecord::Base end def reset_cache_and_import_attrs - ProjectCacheWorker.perform_async(self.id) + run_after_commit do + ProjectCacheWorker.perform_async(self.id) + end self.import_data&.destroy end @@ -1226,6 +1228,7 @@ class Project < ActiveRecord::Base { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: path, public: true }, { key: 'CI_PROJECT_PATH', value: path_with_namespace, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: path_with_namespace.parameterize, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: web_url, public: true } ] diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 882e2fa0594..6c3358685fe 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -8,6 +8,7 @@ class Snippet < ActiveRecord::Base include Awardable include Mentionable include Spammable + include Editable cache_markdown_field :title, pipeline: :single_line cache_markdown_field :content diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 87398303c68..fb07298c6c2 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -4,22 +4,25 @@ class GroupPolicy < BasePolicy return unless @user globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) - member = @subject.users_with_parents.include?(@user) - owner = @user.admin? || @subject.has_owner?(@user) - master = owner || @subject.has_master?(@user) + access_level = @subject.max_member_access_for_user(@user) + owner = access_level >= GroupMember::OWNER + master = access_level >= GroupMember::MASTER + reporter = access_level >= GroupMember::REPORTER can_read = false can_read ||= globally_viewable - can_read ||= member - can_read ||= @user.admin? + can_read ||= access_level >= GroupMember::GUEST can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? can! :read_group if can_read + if reporter + can! :admin_label + end + # Only group masters and group owners can create new projects if master can! :create_projects can! :admin_milestones - can! :admin_label end # Only group owner and administrators can admin group @@ -31,7 +34,7 @@ class GroupPolicy < BasePolicy can! :create_subgroup if @user.can_create_group end - if globally_viewable && @subject.request_access_enabled && !member + if globally_viewable && @subject.request_access_enabled && access_level == GroupMember::NO_ACCESS can! :request_access end end diff --git a/app/presenters/conversational_development_index/metric_presenter.rb b/app/presenters/conversational_development_index/metric_presenter.rb new file mode 100644 index 00000000000..bb65ba2646b --- /dev/null +++ b/app/presenters/conversational_development_index/metric_presenter.rb @@ -0,0 +1,144 @@ +module ConversationalDevelopmentIndex + class MetricPresenter < Gitlab::View::Presenter::Simple + def cards + [ + Card.new( + metric: subject, + title: 'Issues', + description: 'created per active user', + feature: 'issues', + blog: 'https://www2.deloitte.com/content/dam/Deloitte/se/Documents/technology-media-telecommunications/deloitte-digital-collaboration.pdf' + ), + Card.new( + metric: subject, + title: 'Comments', + description: 'created per active user', + feature: 'notes', + blog: 'http://conversationaldevelopment.com/why/' + ), + Card.new( + metric: subject, + title: 'Milestones', + description: 'created per active user', + feature: 'milestones', + blog: 'http://conversationaldevelopment.com/shorten-cycle/', + docs: help_page_path('user/project/milestones/index') + ), + Card.new( + metric: subject, + title: 'Boards', + description: 'created per active user', + feature: 'boards', + blog: 'http://jpattonassociates.com/user-story-mapping/', + docs: help_page_path('user/project/issue_board') + ), + Card.new( + metric: subject, + title: 'Merge Requests', + description: 'per active user', + feature: 'merge_requests', + blog: 'https://8thlight.com/blog/uncle-bob/2013/02/01/The-Humble-Craftsman.html', + docs: help_page_path('user/project/merge_requests/index') + ), + Card.new( + metric: subject, + title: 'Pipelines', + description: 'created per active user', + feature: 'ci_pipelines', + blog: 'https://martinfowler.com/bliki/ContinuousDelivery.html', + docs: help_page_path('ci/README') + ), + Card.new( + metric: subject, + title: 'Environments', + description: 'created per active user', + feature: 'environments', + blog: 'https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/', + docs: help_page_path('ci/environments') + ), + Card.new( + metric: subject, + title: 'Deployments', + description: 'created per active user', + feature: 'deployments', + blog: 'https://puppet.com/blog/continuous-delivery-vs-continuous-deployment-what-s-diff' + ), + Card.new( + metric: subject, + title: 'Monitoring', + description: 'fraction of all projects', + feature: 'projects_prometheus_active', + blog: 'https://prometheus.io/docs/introduction/overview/', + docs: help_page_path('user/project/integrations/prometheus') + ), + Card.new( + metric: subject, + title: 'Service Desk', + description: 'issues created per active user', + feature: 'service_desk_issues', + blog: 'http://blogs.forrester.com/kate_leggett/17-01-30-top_trends_for_customer_service_in_2017_operations_become_smarter_and_more_strategic', + docs: 'https://docs.gitlab.com/ee/user/project/service_desk.html' + ) + ] + end + + def idea_to_production_steps + [ + IdeaToProductionStep.new( + metric: subject, + title: 'Idea', + features: %w(issues) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Issue', + features: %w(issues notes) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Plan', + features: %w(milestones boards) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Code', + features: %w(merge_requests) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Commit', + features: %w(merge_requests) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Test', + features: %w(ci_pipelines) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Review', + features: %w(ci_pipelines environments) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Staging', + features: %w(environments deployments) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Production', + features: %w(deployments) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Feedback', + features: %w(projects_prometheus_active service_desk_issues) + ) + ] + end + + def average_percentage_score + cards.sum(&:percentage_score) / cards.size.to_f + end + end +end diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb index dde17aa68b8..cb55c98f7c6 100644 --- a/app/serializers/build_artifact_entity.rb +++ b/app/serializers/build_artifact_entity.rb @@ -1,14 +1,39 @@ class BuildArtifactEntity < Grape::Entity include RequestAwareEntity - expose :name do |build| - build.name + expose :name do |job| + job.name end - expose :path do |build| + expose :artifacts_expired?, as: :expired + expose :artifacts_expire_at, as: :expire_at + + expose :path do |job| download_namespace_project_job_artifacts_path( - build.project.namespace, - build.project, - build) + project.namespace, + project, + job) + end + + expose :keep_path, if: -> (*) { job.has_expiring_artifacts? } do |job| + keep_namespace_project_job_artifacts_path( + project.namespace, + project, + job) + end + + expose :browse_path do |job| + browse_namespace_project_job_artifacts_path( + project.namespace, + project, + job) + end + + private + + alias_method :job, :object + + def project + job.project end end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb new file mode 100644 index 00000000000..0063920e603 --- /dev/null +++ b/app/serializers/build_details_entity.rb @@ -0,0 +1,50 @@ +class BuildDetailsEntity < BuildEntity + expose :coverage, :erased_at, :duration + expose :tag_list, as: :tags + + expose :user, using: UserEntity + + expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity + expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build| + erase_namespace_project_job_path(project.namespace, project, build) + end + + expose :artifacts, using: BuildArtifactEntity + expose :runner, using: RunnerEntity + expose :pipeline, using: PipelineEntity + + expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do + expose :iid do |build| + build.merge_request.iid + end + + expose :path do |build| + namespace_project_merge_request_path(project.namespace, project, build.merge_request) + end + end + + expose :new_issue_path, if: -> (*) { can?(request.current_user, :create_issue, project) && build.failed? } do |build| + new_namespace_project_issue_path(project.namespace, project, issue: build_failed_issue_options) + end + + expose :raw_path do |build| + raw_namespace_project_build_path(project.namespace, project, build) + end + + private + + def build_failed_issue_options + { + title: "Build Failed ##{build.id}", + description: namespace_project_job_url(project.namespace, project, build) + } + end + + def current_user + request.current_user + end + + def project + build.project + end +end diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index 05dd8270e92..c01efa9dd5c 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -8,7 +8,7 @@ class BuildEntity < Grape::Entity path_to(:namespace_project_job, build) end - expose :retry_path do |build| + expose :retry_path, if: -> (*) { build&.retryable? } do |build| path_to(:retry_namespace_project_job, build) end diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index f7eb75395b5..7bb981041cc 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -29,7 +29,7 @@ class MergeRequestEntity < IssuableEntity expose :merge_commit_sha expose :merge_commit_message - expose :head_pipeline, with: PipelineEntity, as: :pipeline + expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline # Booleans expose :work_in_progress?, as: :work_in_progress diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb new file mode 100644 index 00000000000..d58572a5f87 --- /dev/null +++ b/app/serializers/pipeline_details_entity.rb @@ -0,0 +1,7 @@ +class PipelineDetailsEntity < PipelineEntity + expose :details do + expose :stages, using: StageEntity + expose :artifacts, using: BuildArtifactEntity + expose :manual_actions, using: BuildActionEntity + end +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 486f8c36fbd..6d1fd9d459f 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -7,6 +7,8 @@ class PipelineEntity < Grape::Entity expose :coverage expose :source + expose :created_at, :updated_at + expose :path do |pipeline| namespace_project_pipeline_path( pipeline.project.namespace, @@ -14,15 +16,6 @@ class PipelineEntity < Grape::Entity pipeline) end - expose :details do - expose :detailed_status, as: :status, with: StatusEntity - expose :duration - expose :finished_at - expose :stages, using: StageEntity - expose :artifacts, using: BuildArtifactEntity - expose :manual_actions, using: BuildActionEntity - end - expose :flags do expose :latest?, as: :latest expose :stuck?, as: :stuck @@ -31,6 +24,12 @@ class PipelineEntity < Grape::Entity expose :can_cancel?, as: :cancelable end + expose :details do + expose :detailed_status, as: :status, with: StatusEntity + expose :duration + expose :finished_at + end + expose :ref do expose :name do |pipeline| pipeline.ref @@ -47,7 +46,6 @@ class PipelineEntity < Grape::Entity end expose :commit, using: CommitEntity - expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? } expose :retry_path, if: -> (*) { can_retry? } do |pipeline| retry_namespace_project_pipeline_path(pipeline.project.namespace, @@ -61,7 +59,7 @@ class PipelineEntity < Grape::Entity pipeline.id) end - expose :created_at, :updated_at + expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? } private diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index e37af63774c..b428ff69fe8 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,7 +1,7 @@ class PipelineSerializer < BaseSerializer InvalidResourceError = Class.new(StandardError) - entity PipelineEntity + entity PipelineDetailsEntity def with_pagination(request, response) tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } diff --git a/app/serializers/runner_entity.rb b/app/serializers/runner_entity.rb new file mode 100644 index 00000000000..ed7dacc2dbd --- /dev/null +++ b/app/serializers/runner_entity.rb @@ -0,0 +1,18 @@ +class RunnerEntity < Grape::Entity + include RequestAwareEntity + + expose :id, :description + + expose :edit_path, + if: -> (*) { can?(request.current_user, :admin_build, project) && runner.specific? } do |runner| + edit_namespace_project_runner_path(project.namespace, project, runner) + end + + private + + alias_method :runner, :object + + def project + request.project + end +end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index 47f9b2c621c..46823418bb0 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -1,71 +1,59 @@ -class CreateDeploymentService < BaseService - def execute(deployable = nil) +class CreateDeploymentService + attr_reader :job + + delegate :expanded_environment_name, + :environment_url, + :project, + to: :job + + def initialize(job) + @job = job + end + + def execute return unless executable? ActiveRecord::Base.transaction do - @deployable = deployable + environment.external_url = environment_url if environment_url + environment.fire_state_event(action) - @environment = environment - @environment.external_url = expanded_url if expanded_url - @environment.fire_state_event(action) + return unless environment.save + return if environment.stopped? - return unless @environment.save - return if @environment.stopped? - - deploy.tap do |deployment| - deployment.update_merge_request_metrics! - end + deploy.tap(&:update_merge_request_metrics!) end end private def executable? - project && name.present? + project && job.environment.present? && environment end def deploy project.deployments.create( - environment: @environment, - ref: params[:ref], - tag: params[:tag], - sha: params[:sha], - user: current_user, - deployable: @deployable, - on_stop: options[:on_stop]) + environment: environment, + ref: job.ref, + tag: job.tag, + sha: job.sha, + user: job.user, + deployable: job, + on_stop: on_stop) end def environment - @environment ||= project.environments.find_or_create_by(name: expanded_name) - end - - def expanded_name - ExpandVariables.expand(name, variables) - end - - def expanded_url - return unless url - - @expanded_url ||= ExpandVariables.expand(url, variables) - end - - def name - params[:environment] - end - - def url - options[:url] + @environment ||= job.persisted_environment end - def options - params[:options] || {} + def environment_options + @environment_options ||= job.options&.dig(:environment) || {} end - def variables - params[:variables] || [] + def on_stop + environment_options[:on_stop] end def action - options[:action] || 'start' + environment_options[:action] || 'start' end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 06d8d143231..e2b2660ea71 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -7,11 +7,9 @@ module Projects DELETED_FLAG = '+deleted'.freeze def async_execute - project.transaction do - project.update_attribute(:pending_delete, true) - job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params) - Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}") - end + project.update_attribute(:pending_delete, true) + job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params) + Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}") end def execute @@ -62,7 +60,11 @@ module Projects if gitlab_shell.mv_repository(project.repository_storage_path, path, new_path) log_info("Repository \"#{path}\" moved to \"#{new_path}\"") - GitlabShellWorker.perform_in(5.minutes, :remove_repository, project.repository_storage_path, new_path) + + project.run_after_commit do + # self is now project + GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage_path, new_path) + end else false end diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb new file mode 100644 index 00000000000..17857ca62f2 --- /dev/null +++ b/app/services/submit_usage_ping_service.rb @@ -0,0 +1,41 @@ +class SubmitUsagePingService + URL = 'https://version.gitlab.com/usage_data'.freeze + + include Gitlab::CurrentSettings + + def execute + return false unless current_application_settings.usage_ping_enabled? + + response = HTTParty.post( + URL, + body: Gitlab::UsageData.to_json(force_refresh: true), + headers: { 'Content-type' => 'application/json' } + ) + + store_metrics(response) + + true + rescue HTTParty::Error => e + Rails.logger.info "Unable to contact GitLab, Inc.: #{e}" + + false + end + + private + + def store_metrics(response) + return unless response['conv_index'].present? + + ConversationalDevelopmentIndex::Metric.create!( + response['conv_index'].slice( + 'leader_issues', 'instance_issues', 'leader_notes', 'instance_notes', + 'leader_milestones', 'instance_milestones', 'leader_boards', 'instance_boards', + 'leader_merge_requests', 'instance_merge_requests', 'leader_ci_pipelines', + 'instance_ci_pipelines', 'leader_environments', 'instance_environments', + 'leader_deployments', 'instance_deployments', 'leader_projects_prometheus_active', + 'instance_projects_prometheus_active', 'leader_service_desk_issues', + 'instance_service_desk_issues' + ) + ) + end +end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index 95a891111e1..02589959c2f 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -12,4 +12,20 @@ class LfsObjectUploader < GitlabUploader def filename model.oid[4..-1] end + + def work_dir + File.join(Gitlab.config.lfs.storage_path, 'tmp', 'work') + end + + private + + # To prevent LFS files from moving across filesystems, override the default + # implementation: + # http://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L181-L183 + def workfile_path(for_file = original_filename) + # To be safe, keep this directory outside of the the cache directory + # because calling CarrierWave.clean_cache_files! will remove any files in + # the cache directory. + File.join(work_dir, @cache_id, version_name.to_s, for_file) + end end diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index ac36bb5bb17..e5842bd1ea0 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "Background Jobs" -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title Background Jobs diff --git a/app/views/admin/conversational_development_index/_callout.html.haml b/app/views/admin/conversational_development_index/_callout.html.haml new file mode 100644 index 00000000000..33a4dab1e00 --- /dev/null +++ b/app/views/admin/conversational_development_index/_callout.html.haml @@ -0,0 +1,13 @@ +.prepend-top-default +.user-callout{ data: { uid: 'convdev_intro_callout_dismissed' } } + .bordered-box.landing.content-block + %button.btn.btn-default.close.js-close-callout{ type: 'button', + 'aria-label' => 'Dismiss ConvDev introduction' } + = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') + .user-callout-copy + %h4 + Introducing Your Conversational Development Index + %p + Your Conversational Development Index gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers. + .svg-container.convdev + = custom_icon('convdev_overview') diff --git a/app/views/admin/conversational_development_index/_card.html.haml b/app/views/admin/conversational_development_index/_card.html.haml new file mode 100644 index 00000000000..6c8688e06ae --- /dev/null +++ b/app/views/admin/conversational_development_index/_card.html.haml @@ -0,0 +1,25 @@ +.convdev-card-wrapper + .convdev-card{ class: "convdev-card-#{score_level(card.percentage_score)}" } + .convdev-card-title + %h3 + = card.title + .text-light + = card.description + .card-scores + .card-score + .card-score-value + = format_score(card.instance_score) + .card-score-name You + .card-score + .card-score-value + = format_score(card.leader_score) + .card-score-name Lead + .card-score-big + = number_to_percentage(card.percentage_score, precision: 1) + .card-buttons + - if card.blog + %a{ href: card.blog } + = icon('info-circle', 'aria-hidden' => 'true') + - if card.docs + %a{ href: card.docs } + = icon('question-circle', 'aria-hidden' => 'true') diff --git a/app/views/admin/conversational_development_index/_disabled.html.haml b/app/views/admin/conversational_development_index/_disabled.html.haml new file mode 100644 index 00000000000..975d7df3da6 --- /dev/null +++ b/app/views/admin/conversational_development_index/_disabled.html.haml @@ -0,0 +1,9 @@ +.container.convdev-empty + .col-sm-6.col-sm-push-3.text-center + = custom_icon('convdev_no_index') + %h4 Usage ping is not enabled + %p + ConvDev is only shown when the + = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank' + is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective + = link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' diff --git a/app/views/admin/conversational_development_index/_no_data.html.haml b/app/views/admin/conversational_development_index/_no_data.html.haml new file mode 100644 index 00000000000..b23d2b5ec3a --- /dev/null +++ b/app/views/admin/conversational_development_index/_no_data.html.haml @@ -0,0 +1,7 @@ +.container.convdev-empty + .col-sm-6.col-sm-push-3.text-center + = custom_icon('convdev_no_data') + %h4 Data is still calculating... + %p + In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index. + = link_to 'Learn more', help_page_path('user/admin_area/monitoring/convdev'), target: '_blank' diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml new file mode 100644 index 00000000000..833d4c612f8 --- /dev/null +++ b/app/views/admin/conversational_development_index/show.html.haml @@ -0,0 +1,35 @@ +- @no_container = true +- page_title 'ConvDev Index' + += render 'admin/monitoring/head' + +.container + - if show_callout?('convdev_intro_callout_dismissed') + = render 'callout' + + .prepend-top-default + - if !current_application_settings.usage_ping_enabled + = render 'disabled' + - elsif @metric.blank? + = render 'no_data' + - else + .convdev + .convdev-header + %h2.convdev-header-title{ class: "convdev-#{score_level(@metric.average_percentage_score)}-score" } + = number_to_percentage(@metric.average_percentage_score, precision: 1) + .convdev-header-subtitle + index + %br + score + = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/admin_area/monitoring/convdev') + + .convdev-cards.card-container + - @metric.cards.each do |card| + = render 'card', card: card + + .convdev-steps.visible-lg + - @metric.idea_to_production_steps.each_with_index do |step, index| + .convdev-step{ class: "convdev-#{score_level(step.percentage_score)}-score" } + = custom_icon("i2p_step_#{index + 1}") + %h4.convdev-step-title + = step.title diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 4deccf4aa93..8adb966064c 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "Health Check" -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 5e585ce789b..487f1cf5c4f 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -3,7 +3,7 @@ - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, Gitlab::EnvironmentLogger, Gitlab::SidekiqLogger, Gitlab::RepositoryCheckLogger] -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } %ul.nav-links.log-tabs diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/monitoring/_head.html.haml index b3530915068..901e30275fd 100644 --- a/app/views/admin/background_jobs/_head.html.haml +++ b/app/views/admin/monitoring/_head.html.haml @@ -3,6 +3,10 @@ = render 'shared/nav_scroll' .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } + = nav_link(controller: :conversational_development_index) do + = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do + %span + ConvDev Index = nav_link(controller: :system_info) do = link_to admin_system_info_path, title: 'System Info' do %span diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml index c7b63d9de98..b7db18b2d32 100644 --- a/app/views/admin/requests_profiles/index.html.haml +++ b/app/views/admin/requests_profiles/index.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title 'Requests Profiles' -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index 9b9559c7fe5..fd0281e4961 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "System Info" -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } .prepend-top-default diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 2890ae7173b..5e63a61e21b 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -9,7 +9,7 @@ = render "projects/last_push" %div{ class: container_class } - - if show_user_callout? + - if show_callout?('user_callout_dismissed') = render 'shared/user_callout' - if @projects.any? || params[:name] diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 86779eeaf15..6df0adfd742 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -9,8 +9,8 @@ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span Overview - = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_system_info_path, title: 'Monitoring' do + = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do + = link_to admin_conversational_development_index_path, title: 'Monitoring' do %span Monitoring = nav_link(controller: :broadcast_messages) do diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 7bf271c2fc5..d909b0bfbbd 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -63,7 +63,7 @@ .wiki= markdown_field(@issue, :description) %textarea.hidden.js-task-list-field= @issue.description - = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') + = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') #merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } } // This element is filled in using JavaScript. diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml index 8308baa7829..17ffcba69d8 100644 --- a/app/views/shared/_user_callout.html.haml +++ b/app/views/shared/_user_callout.html.haml @@ -1,4 +1,4 @@ -.user-callout +.user-callout{ data: { uid: 'user_callout_dismissed' } } .bordered-box.landing.content-block %button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss customize experience box' } diff --git a/app/views/shared/icons/_convdev_no_data.svg b/app/views/shared/icons/_convdev_no_data.svg new file mode 100644 index 00000000000..ed32b2333e7 --- /dev/null +++ b/app/views/shared/icons/_convdev_no_data.svg @@ -0,0 +1,40 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="360" height="220" viewBox="0 0 360 220"> + <g fill="none" fill-rule="evenodd"> + <path fill="#000" fill-opacity=".02" d="M125 44V24.003C125 18.48 129.483 14 135.005 14h89.99C230.52 14 235 18.477 235 24.003V43h84.992C326.624 43 332 48.372 332 55.002v144.996c0 6.63-5.38 12.002-12.008 12.002h-85.984c-6.632 0-12.008-5.372-12.008-12.002V183h-78v17.002c0 6.626-5.38 11.998-12.008 11.998H46.008C39.376 212 34 206.624 34 200.002V55.998C34 49.372 39.38 44 46.008 44H125z"/> + <g transform="translate(214 36)"> + <rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/> + <path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77.796-.766.82-2.032.055-2.828-.766-.796-2.032-.82-2.828-.055C1.347 5.6 0 8.7 0 12.006c0 1.105.895 2 2 2s2-.895 2-2zM14.388 4h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm17.51.227c2.115.514 3.93 1.88 5.022 3.756.556.955 1.78 1.28 2.735.724.954-.556 1.278-1.78.723-2.735-1.636-2.813-4.356-4.86-7.534-5.632-1.073-.26-2.155.397-2.416 1.47-.26 1.074.397 2.156 1.47 2.417zM110 16.78v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm-.024 17.844c-.17 2.186-1.227 4.18-2.903 5.558-.853.702-.976 1.962-.275 2.815.7.854 1.962.977 2.815.275 2.51-2.062 4.096-5.056 4.35-8.338.086-1.1-.737-2.063-1.838-2.15-1.102-.084-2.064.74-2.15 1.84zM98.826 168h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-17.334-.4c-2.063-.68-3.77-2.186-4.71-4.143-.477-.996-1.67-1.416-2.667-.938-.996.476-1.416 1.67-.938 2.667 1.41 2.936 3.964 5.19 7.063 6.21 1.05.347 2.18-.223 2.526-1.272.346-1.05-.224-2.18-1.274-2.526zM4 154.434v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2z"/> + <path fill="#F0EDF8" fill-rule="nonzero" d="M57 111c-11.598 0-21-9.402-21-21s9.402-21 21-21 21 9.402 21 21-9.402 21-21 21zm0-4c9.39 0 17-7.61 17-17s-7.61-17-17-17-17 7.61-17 17 7.61 17 17 17z"/> + <path fill="#6B4FBB" d="M58 88v-6.997c0-1.11-.895-2.003-2-2.003-1.112 0-2 .897-2 2.003v8.994c0 1.11.895 2.003 2 2.003.174 0 .343-.022.503-.063.162.04.33.063.506.063h7.98C66.1 92 67 91.105 67 90c0-1.112-.9-2-2.01-2H58z"/> + <rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/> + <path fill="#EEE" d="M21 16c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C21.895 18 21 17.112 21 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C34.895 18 34 17.112 34 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C47.895 18 47 17.112 47 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C60.895 18 60 17.112 60 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C73.895 18 73 17.112 73 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C86.895 18 86 17.112 86 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C99.895 18 99 17.112 99 16z"/> + </g> + <g transform="translate(118 7)"> + <rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/> + <path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77.796-.766.82-2.032.055-2.828-.766-.796-2.032-.82-2.828-.055C1.347 5.6 0 8.7 0 12.006c0 1.105.895 2 2 2s2-.895 2-2zM14.388 4h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm17.51.227c2.115.514 3.93 1.88 5.022 3.756.556.955 1.78 1.28 2.735.724.954-.556 1.278-1.78.723-2.735-1.636-2.813-4.356-4.86-7.534-5.632-1.073-.26-2.155.397-2.416 1.47-.26 1.074.397 2.156 1.47 2.417zM110 16.78v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm-.024 17.844c-.17 2.186-1.227 4.18-2.903 5.558-.853.702-.976 1.962-.275 2.815.7.854 1.962.977 2.815.275 2.51-2.062 4.096-5.056 4.35-8.338.086-1.1-.737-2.063-1.838-2.15-1.102-.084-2.064.74-2.15 1.84zM98.826 168h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-17.334-.4c-2.063-.68-3.77-2.186-4.71-4.143-.477-.996-1.67-1.416-2.667-.938-.996.476-1.416 1.67-.938 2.667 1.41 2.936 3.964 5.19 7.063 6.21 1.05.347 2.18-.223 2.526-1.272.346-1.05-.224-2.18-1.274-2.526zM4 154.434v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2z"/> + <g fill-rule="nonzero"> + <path fill="#F0EDF8" d="M57 112c-12.15 0-22-9.85-22-22s9.85-22 22-22 22 9.85 22 22-9.85 22-22 22zm0-6c8.837 0 16-7.163 16-16s-7.163-16-16-16-16 7.163-16 16 7.163 16 16 16z"/> + <path fill="#6B4FBB" d="M41.692 105.8C45.768 109.75 51.21 112 57 112c12.15 0 22-9.85 22-22s-9.85-22-22-22v6c8.837 0 16 7.163 16 16s-7.163 16-16 16c-4.215 0-8.166-1.633-11.133-4.508l-4.175 4.31z"/> + </g> + <path fill="#EEE" d="M8 16c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2H9.998C8.895 18 8 17.112 8 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C21.895 18 21 17.112 21 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C34.895 18 34 17.112 34 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C47.895 18 47 17.112 47 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C60.895 18 60 17.112 60 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C73.895 18 73 17.112 73 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C86.895 18 86 17.112 86 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004C99.895 18 99 17.112 99 16z"/> + </g> + <g transform="translate(26 36)"> + <rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/> + <path fill="#EEE" fill-rule="nonzero" d="M4 12.006v147.988C4 164.42 7.58 168 12.005 168h89.99c4.42 0 8.005-3.586 8.005-8.006V12.006C110 7.58 106.42 4 101.995 4h-89.99C7.585 4 4 7.586 4 12.006zm-4 0C0 5.376 5.377 0 12.005 0h89.99C108.628 0 114 5.37 114 12.006v147.988c0 6.63-5.377 12.006-12.005 12.006h-89.99C5.372 172 0 166.63 0 159.994V12.006z"/> + <g transform="translate(21 82)"> + <rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/> + <rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/> + </g> + <g transform="translate(69 82)"> + <rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/> + <rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/> + </g> + <g transform="translate(38 42)"> + <rect width="22" height="4" x="8" fill="#FEE1D3" rx="2"/> + <rect width="38" height="4" y="12" fill="#FB722E" rx="2"/> + </g> + <path fill="#EEE" d="M4 14h106v4H4z"/> + <path fill="#333" d="M35.724 138h9.696v-2.856h-2.856V122.76h-2.592c-1.08.648-2.136 1.08-3.792 1.392v2.184h2.856v8.808h-3.312V138zm17.736.288c-2.952 0-5.76-2.208-5.76-7.56 0-5.688 2.952-8.256 6.168-8.256 2.016 0 3.48.84 4.44 1.824l-1.848 2.112c-.528-.576-1.488-1.08-2.376-1.08-1.68 0-3.024 1.2-3.144 4.752.792-1.008 2.112-1.608 3.048-1.608 2.616 0 4.536 1.488 4.536 4.704 0 3.168-2.304 5.112-5.064 5.112zm-.072-2.64c1.056 0 1.92-.744 1.92-2.472 0-1.608-.84-2.208-1.992-2.208-.792 0-1.68.432-2.304 1.512.312 2.4 1.32 3.168 2.376 3.168zM63.9 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/> + </g> + </g> +</svg> diff --git a/app/views/shared/icons/_convdev_no_index.svg b/app/views/shared/icons/_convdev_no_index.svg new file mode 100644 index 00000000000..95c00e81d10 --- /dev/null +++ b/app/views/shared/icons/_convdev_no_index.svg @@ -0,0 +1,67 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200"> + <g fill="none" fill-rule="evenodd" transform="translate(3 11)"> + <rect width="110" height="168" x="6" y="8" fill="#000" fill-opacity=".02" rx="10"/> + <g transform="translate(0 2)"> + <rect width="110" height="168" fill="#FFF" rx="10"/> + <path fill="#EEE" fill-rule="nonzero" d="M2 10.006v147.988C2 162.42 5.58 166 10.005 166h89.99c4.42 0 8.005-3.586 8.005-8.006V10.006C108 5.58 104.42 2 99.995 2h-89.99C5.585 2 2 5.586 2 10.006zm-4 0C-2 3.376 3.377-2 10.005-2h89.99C106.628-2 112 3.37 112 10.006v147.988c0 6.63-5.377 12.006-12.005 12.006h-89.99C3.372 170-2 164.63-2 157.994V10.006z"/> + <g transform="translate(19 80)"> + <rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/> + <rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/> + </g> + <g transform="translate(67 80)"> + <rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/> + <rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/> + </g> + <g transform="translate(36 40)"> + <rect width="22" height="4" x="8" fill="#FEE1D3" rx="2"/> + <rect width="38" height="4" y="12" fill="#FB722E" rx="2"/> + </g> + <path fill="#EEE" d="M2 12h106v4H2z"/> + <path fill="#333" d="M38.048 127.792c.792 0 1.68-.432 2.28-1.512-.312-2.4-1.296-3.168-2.376-3.168-1.032 0-1.92.744-1.92 2.472 0 1.608.864 2.208 2.016 2.208zm-.552 8.496c-2.016 0-3.504-.864-4.464-1.824l1.872-2.112c.504.576 1.464 1.08 2.352 1.08 1.704 0 3.024-1.2 3.144-4.752-.792 1.008-2.112 1.608-3.048 1.608-2.592 0-4.536-1.488-4.536-4.704 0-3.168 2.304-5.112 5.064-5.112 2.952 0 5.784 2.208 5.784 7.56 0 5.688-2.976 8.256-6.168 8.256zm13.488 0c-3.048 0-5.304-1.704-5.304-4.176 0-1.848 1.152-2.976 2.592-3.744v-.096c-1.176-.888-2.04-1.992-2.04-3.6 0-2.592 2.04-4.2 4.872-4.2 2.784 0 4.632 1.656 4.632 4.176 0 1.464-.936 2.64-1.992 3.336v.096c1.464.792 2.64 1.968 2.64 3.984 0 2.4-2.16 4.224-5.4 4.224zm.96-9.168c.6-.696.936-1.44.936-2.232 0-1.176-.696-1.968-1.848-1.968-.936 0-1.704.576-1.704 1.752 0 1.248 1.056 1.848 2.616 2.448zm-.888 6.72c1.176 0 2.04-.624 2.04-1.896 0-1.344-1.296-1.848-3.216-2.664-.672.624-1.176 1.488-1.176 2.424 0 1.344 1.08 2.136 2.352 2.136zm10.8-3.84c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/> + </g> + <g transform="translate(122)"> + <rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/> + <path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77.796-.766.82-2.032.055-2.828-.766-.796-2.032-.82-2.828-.055C1.347 5.6 0 8.7 0 12.006c0 1.105.895 2 2 2s2-.895 2-2zM14.388 4h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm17.51.227c2.115.514 3.93 1.88 5.022 3.756.556.955 1.78 1.28 2.735.724.954-.556 1.278-1.78.723-2.735-1.636-2.813-4.356-4.86-7.534-5.632-1.073-.26-2.155.397-2.416 1.47-.26 1.074.397 2.156 1.47 2.417zM110 16.78v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm-.024 17.844c-.17 2.186-1.227 4.18-2.903 5.558-.853.702-.976 1.962-.275 2.815.7.854 1.962.977 2.815.275 2.51-2.062 4.096-5.056 4.35-8.338.086-1.1-.737-2.063-1.838-2.15-1.102-.084-2.064.74-2.15 1.84zM98.826 168h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-17.334-.4c-2.063-.68-3.77-2.186-4.71-4.143-.477-.996-1.67-1.416-2.667-.938-.996.476-1.416 1.67-.938 2.667 1.41 2.936 3.964 5.19 7.063 6.21 1.05.347 2.18-.223 2.526-1.272.346-1.05-.224-2.18-1.274-2.526zM4 154.434v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2z"/> + <g transform="translate(21 82)"> + <rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/> + <rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/> + </g> + <g transform="translate(69 82)"> + <rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/> + <rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/> + </g> + <path fill="#FEE1D3" d="M44 44c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C44.894 46 44 45.112 44 44zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C54.894 46 54 45.112 54 44zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C64.894 46 64 45.112 64 44zM34 56c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C34.894 58 34 57.112 34 56zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C44.894 58 44 57.112 44 56zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C54.894 58 54 57.112 54 56zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C64.894 58 64 57.112 64 56zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C74.894 58 74 57.112 74 56z"/> + <rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="21" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="34" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="47" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="60" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/> + <path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/> + </g> + <g transform="translate(243)"> + <rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/> + <path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77.796-.766.82-2.032.055-2.828-.766-.796-2.032-.82-2.828-.055C1.347 5.6 0 8.7 0 12.006c0 1.105.895 2 2 2s2-.895 2-2zM14.388 4h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm18 0h8c1.104 0 2-.895 2-2s-.896-2-2-2h-8c-1.105 0-2 .895-2 2s.895 2 2 2zm17.51.227c2.115.514 3.93 1.88 5.022 3.756.556.955 1.78 1.28 2.735.724.954-.556 1.278-1.78.723-2.735-1.636-2.813-4.356-4.86-7.534-5.632-1.073-.26-2.155.397-2.416 1.47-.26 1.074.397 2.156 1.47 2.417zM110 16.78v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm0 18v8c0 1.104.895 2 2 2s2-.896 2-2v-8c0-1.105-.895-2-2-2s-2 .895-2 2zm-.024 17.844c-.17 2.186-1.227 4.18-2.903 5.558-.853.702-.976 1.962-.275 2.815.7.854 1.962.977 2.815.275 2.51-2.062 4.096-5.056 4.35-8.338.086-1.1-.737-2.063-1.838-2.15-1.102-.084-2.064.74-2.15 1.84zM98.826 168h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-18 0h-8c-1.104 0-2 .895-2 2s.896 2 2 2h8c1.105 0 2-.895 2-2s-.895-2-2-2zm-17.334-.4c-2.063-.68-3.77-2.186-4.71-4.143-.477-.996-1.67-1.416-2.667-.938-.996.476-1.416 1.67-.938 2.667 1.41 2.936 3.964 5.19 7.063 6.21 1.05.347 2.18-.223 2.526-1.272.346-1.05-.224-2.18-1.274-2.526zM4 154.434v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2zm0-18v-8c0-1.104-.895-2-2-2s-2 .896-2 2v8c0 1.105.895 2 2 2s2-.895 2-2z"/> + <path fill="#FEE1D3" d="M44 44c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C44.894 46 44 45.112 44 44zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C54.894 46 54 45.112 54 44zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C64.894 46 64 45.112 64 44zM34 56c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C34.894 58 34 57.112 34 56zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C44.894 58 44 57.112 44 56zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C54.894 58 54 57.112 54 56zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C64.894 58 64 57.112 64 56zm10 0c0-1.105.898-2 1.998-2h2.004c1.104 0 1.998.888 1.998 2 0 1.105-.898 2-1.998 2h-2.004C74.894 58 74 57.112 74 56z"/> + <g transform="translate(21 82)"> + <rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/> + <rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/> + </g> + <g transform="translate(69 82)"> + <rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/> + <rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/> + </g> + <rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="21" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="34" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="47" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="60" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/> + <rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/> + <path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/> + </g> + </g> +</svg> diff --git a/app/views/shared/icons/_convdev_overview.svg b/app/views/shared/icons/_convdev_overview.svg new file mode 100644 index 00000000000..2f31113bad7 --- /dev/null +++ b/app/views/shared/icons/_convdev_overview.svg @@ -0,0 +1,64 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="208" height="127" viewBox="0 0 208 127" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <rect id="a" width="58" height="98" y="17" rx="6"/> + <rect id="b" width="58" height="98" x="3.5" y="17" rx="6"/> + <rect id="c" width="58" height="98.394" rx="6"/> + </defs> + <g fill="none" fill-rule="evenodd" transform="translate(1)"> + <path fill="#000" fill-opacity=".06" fill-rule="nonzero" d="M16 11.06c0-1.39.56-2.69 1.534-3.635.398-.386.41-1.025.027-1.426-.382-.402-1.015-.414-1.413-.028C14.785 7.294 14 9.116 14 11.062c0 .556.448 1.007 1 1.007s1-.452 1-1.01zm6.432-5.043h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0H185c1.21 0 2.354.435 3.254 1.215.42.362 1.05.314 1.41-.108.36-.423.312-1.06-.107-1.422C188.297 4.612 186.694 4 185 4h-.568c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zM190 11.932v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008zm0 10.89v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.01v-4.838c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01zm0 10.89v4.84c0 .555.448 1.007 1 1.007s1-.453 1-1.01v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.008zm0 10.888v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008V44.6c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008zm0 10.89v4.84c0 .556.448 1.007 1 1.007s1-.45 1-1.008v-4.84c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01zm0 10.89v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01zm0 10.888v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008zm0 10.89v4.84c0 .556.448 1.007 1 1.007s1-.45 1-1.008v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.007zm0 10.888v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008zm-.24 21.446c-.42 1.304-1.353 2.385-2.572 2.985-.497.244-.703.847-.46 1.348.24.5.84.708 1.336.464 1.707-.84 3.013-2.35 3.598-4.178.17-.53-.12-1.098-.644-1.27-.526-.17-1.09.12-1.26.65zm-8.063 3.49h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.577-.116c-1.33-.295-2.48-1.13-3.19-2.3-.287-.474-.902-.623-1.373-.333-.472.29-.62.91-.332 1.386.99 1.632 2.6 2.8 4.465 3.215.54.12 1.073-.224 1.192-.768.12-.544-.222-1.082-.762-1.2zM16 105.292v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01zm0-10.89v-4.84c0-.555-.448-1.007-1-1.007s-1 .452-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.007zm0-10.888v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-10.89v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.008v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008zm0-10.89v-4.838c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.01zm0-11.888v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-9.89v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.007v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-10.888v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008zm0-10.89v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01z"/> + <g transform="translate(74)"> + <rect width="58" height="98" y="20" fill="#000" fill-opacity=".02" rx="6"/> + <use fill="#FFF" xlink:href="#a"/> + <rect width="56" height="96" x="1" y="18" stroke="#EEE" stroke-width="2" rx="6"/> + <g transform="translate(16 45.185)"> + <path fill="#333" d="M.59 33.815h5.655V32.15H4.58v-7.225H3.066c-.63.378-1.246.63-2.212.812v1.274H2.52v5.14H.59v1.665zm10.093.168c-1.778 0-3.094-.994-3.094-2.436 0-1.078.67-1.736 1.51-2.184v-.056c-.685-.518-1.19-1.162-1.19-2.1 0-1.512 1.19-2.45 2.843-2.45 1.624 0 2.702.966 2.702 2.436 0 .854-.546 1.54-1.162 1.946v.055c.854.462 1.54 1.148 1.54 2.324 0 1.4-1.26 2.463-3.15 2.463zm.56-5.348c.35-.406.546-.84.546-1.302 0-.686-.407-1.148-1.08-1.148-.545 0-.993.336-.993 1.022 0 .728.616 1.078 1.526 1.428zm-.518 3.92c.686 0 1.19-.364 1.19-1.106 0-.785-.756-1.08-1.876-1.555-.393.364-.687.868-.687 1.414 0 .783.63 1.245 1.372 1.245zm6.3-2.24c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.063 2.282 2.883 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.462.84-1.764s-.378-1.736-.84-1.736c-.462 0-.84.434-.84 1.736s.378 1.764.84 1.764zm.308 4.816l4.928-9.464h1.19l-4.927 9.463h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.883 2.27-2.883 1.315 0 2.28 1.064 2.28 2.884 0 1.835-.965 2.913-2.28 2.913zm0-1.148c.46 0 .84-.462.84-1.764 0-1.3-.38-1.735-.84-1.735-.463 0-.84.434-.84 1.736 0 1.303.377 1.765.84 1.765z"/> + <rect width="13" height="2" x="6" y=".815" fill="#FB722E" rx="1"/> + <path fill="#F0EDF8" d="M3 47.815c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992c-.548 0-.992-.444-.992-1zm0 6c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992c-.548 0-.992-.444-.992-1z"/> + <rect width="20" height="2" x="3" y="6.815" fill="#FEE1D3" rx="1"/> + </g> + <g transform="translate(10.81)"> + <circle cx="18.19" cy="18" r="18" fill="#FFF"/> + <path fill="#F0EDF8" fill-rule="nonzero" d="M18.19 34c8.837 0 16-7.163 16-16s-7.163-16-16-16-16 7.163-16 16 7.163 16 16 16zm0 2c-9.94 0-18-8.06-18-18s8.06-18 18-18 18 8.06 18 18-8.06 18-18 18z"/> + <g transform="translate(10 11)"> + <path fill="#C3B8E3" fill-rule="nonzero" d="M2.19 13.32L5.397 11h7.783c.566 0 1.01-.444 1.01-1V3c0-.55-.45-1-1.01-1H3.2c-.566 0-1.01.444-1.01 1v10.32zM6.045 13l-3.422 2.476C1.28 16.45.19 15.892.19 14.23V3c0-1.657 1.337-3 3.01-3h9.98c1.663 0 3.01 1.342 3.01 3v7c0 1.657-1.337 3-3.01 3H6.045z"/> + <rect width="4" height="2" x="5.19" y="4" fill="#6B4FBB" rx="1"/> + <rect width="6" height="2" x="5.19" y="7" fill="#6B4FBB" rx="1"/> + </g> + </g> + </g> + <g transform="translate(144.5)"> + <rect width="58" height="98" x=".5" y="20" fill="#000" fill-opacity=".02" rx="6"/> + <use fill="#FFF" xlink:href="#b"/> + <rect width="56" height="96" x="4.5" y="18" stroke="#EEE" stroke-width="2" rx="6"/> + <g transform="translate(19 46.185)"> + <path fill="#333" d="M4.01 33.746c1.793 0 3.305-.938 3.305-2.59 0-1.148-.742-1.876-1.764-2.17v-.056c.953-.406 1.485-1.05 1.485-1.974 0-1.554-1.232-2.436-3.066-2.436-1.093 0-1.99.434-2.8 1.134l1.035 1.26c.56-.49 1.036-.784 1.666-.784.7 0 1.093.364 1.093.98 0 .714-.504 1.19-2.1 1.19v1.456c1.932 0 2.394.49 2.394 1.274 0 .672-.574 1.05-1.442 1.05-.756 0-1.414-.378-1.946-.896l-.953 1.302c.644.756 1.652 1.26 3.094 1.26zm4.51-.168h6.257v-1.736h-1.792c-.42 0-1.036.056-1.484.112 1.443-1.512 2.843-3.108 2.843-4.606 0-1.708-1.19-2.828-2.94-2.828-1.274 0-2.1.476-2.982 1.414l1.12 1.106c.45-.476.94-.91 1.583-.91.77 0 1.26.476 1.26 1.344 0 1.26-1.596 2.786-3.864 4.928v1.176zm9.505-3.5c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.064 2.282 2.884 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.462.84-1.764s-.378-1.736-.84-1.736c-.462 0-.84.434-.84 1.736s.378 1.764.84 1.764zm.308 4.816l4.928-9.464h1.19l-4.927 9.464h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.884 2.27-2.884 1.315 0 2.28 1.064 2.28 2.884 0 1.834-.965 2.912-2.28 2.912zm0-1.148c.46 0 .84-.462.84-1.764s-.38-1.736-.84-1.736c-.463 0-.84.434-.84 1.736s.377 1.764.84 1.764z"/> + <rect width="13" height="2.008" x="7.5" fill="#FB722E" rx="1.004"/> + <path fill="#F0EDF8" d="M3.5 47.19c0-.556.455-1.005 1.006-1.005h17.988c.556 0 1.006.445 1.006 1.004 0 .553-.455 1.003-1.006 1.003H4.506c-.556 0-1.006-.446-1.006-1.004zm0 6.023c0-.555.455-1.004 1.006-1.004h17.988c.556 0 1.006.444 1.006 1.003 0 .554-.455 1.004-1.006 1.004H4.506c-.556 0-1.006-.446-1.006-1.004z"/> + <rect width="20" height="2.008" x="4" y="6.024" fill="#FEE1D3" rx="1.004"/> + </g> + <g transform="translate(14.413)"> + <circle cx="18.087" cy="18" r="18" fill="#FFF"/> + <path fill="#F0EDF8" fill-rule="nonzero" d="M18.087 34c8.836 0 16-7.163 16-16s-7.164-16-16-16c-8.837 0-16 7.163-16 16s7.163 16 16 16zm0 2c-9.942 0-18-8.06-18-18s8.058-18 18-18c9.94 0 18 8.06 18 18s-8.06 18-18 18z"/> + <path fill="#C3B8E3" fill-rule="nonzero" d="M18.087 24c3.313 0 6-2.686 6-6s-2.687-6-6-6c-3.314 0-6 2.686-6 6s2.686 6 6 6zm0 2c-4.42 0-8-3.582-8-8s3.58-8 8-8c4.418 0 8 3.582 8 8s-3.582 8-8 8z"/> + <path fill="#6B4FBB" d="M19.087 17v-2c0-.556-.448-1-1-1-.557 0-1 .448-1 1v3c0 .278.11.528.292.71.18.18.43.29.706.29h3c.557 0 1-.448 1-1 0-.556-.447-1-1-1h-2z"/> + </g> + </g> + <rect width="58" height="98" x="3" y="20" fill="#000" fill-opacity=".02" rx="6"/> + <g transform="translate(0 16.754)"> + <use fill="#FFF" xlink:href="#c"/> + <rect width="56" height="96.394" x="1" y="1" stroke="#EEE" stroke-width="2" rx="6"/> + <g transform="translate(16 29.618)"> + <path fill="#333" d="M3.137 27.84c.462 0 .98-.253 1.33-.883-.182-1.4-.756-1.848-1.386-1.848-.6 0-1.12.433-1.12 1.44 0 .94.505 1.29 1.177 1.29zm-.322 4.955C1.64 32.795.77 32.29.21 31.73l1.093-1.23c.294.335.854.63 1.372.63.994 0 1.764-.7 1.834-2.773-.463.588-1.233.938-1.78.938-1.51 0-2.645-.868-2.645-2.744 0-1.847 1.344-2.98 2.954-2.98 1.72 0 3.373 1.287 3.373 4.41 0 3.317-1.736 4.815-3.598 4.815zm8.12 0c-1.722 0-3.36-1.288-3.36-4.41 0-3.318 1.722-4.816 3.598-4.816 1.176 0 2.03.49 2.59 1.063l-1.078 1.232c-.308-.336-.868-.63-1.386-.63-.98 0-1.765.7-1.835 2.772.462-.588 1.232-.938 1.778-.938 1.526 0 2.646.867 2.646 2.743 0 1.848-1.345 2.982-2.955 2.982zm-.042-1.54c.616 0 1.12-.434 1.12-1.442 0-.938-.49-1.288-1.162-1.288-.46 0-.98.252-1.343.882.182 1.4.77 1.848 1.386 1.848zm6.132-2.128c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.065 2.282 2.885 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.463.84-1.765 0-1.302-.378-1.736-.84-1.736-.462 0-.84.433-.84 1.735s.378 1.764.84 1.764zm.308 4.815l4.928-9.464h1.19l-4.927 9.465h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.884 2.27-2.884 1.315 0 2.28 1.063 2.28 2.883 0 1.834-.965 2.912-2.28 2.912zm0-1.148c.46 0 .84-.462.84-1.764s-.38-1.736-.84-1.736c-.463 0-.84.434-.84 1.736s.377 1.764.84 1.764z"/> + <rect width="13" height="2.008" x="6.5" y=".314" fill="#FEE1D3" rx="1.004"/> + <path fill="#F0EDF8" d="M3 46.627c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992c-.548 0-.992-.444-.992-1zm0 6c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992c-.548 0-.992-.444-.992-1z"/> + <rect width="20" height="2" x="3" y="5.627" fill="#FB722E" rx="1"/> + </g> + </g> + <g transform="translate(10.41)"> + <circle cx="18.589" cy="18" r="18" fill="#FFF"/> + <path fill="#F0EDF8" fill-rule="nonzero" d="M18.59 34c8.836 0 16-7.163 16-16s-7.164-16-16-16c-8.837 0-16 7.163-16 16s7.163 16 16 16zm0 2c-9.942 0-18-8.06-18-18s8.058-18 18-18c9.94 0 18 8.06 18 18s-8.06 18-18 18z"/> + <path fill="#C3B8E3" d="M17.05 19.262h3.367l.248-2.808H17.3l-.25 2.808zm-.177 2.008l-.144 1.627c-.06.662-.646 1.2-1.3 1.2h.25c-.658 0-1.144-.534-1.085-1.2l.144-1.627H13.59c-.554 0-1.003-.446-1.003-1.004 0-.555.455-1.004 1.002-1.004h1.325l.248-2.808h-1.15c-.555 0-1.004-.445-1.004-1.004 0-.554.457-1.004 1.004-1.004h1.33l.106-1.2c.058-.66.644-1.198 1.298-1.198h-.25c.66 0 1.145.533 1.086 1.2l-.106 1.198h3.365l.107-1.2c.058-.66.644-1.198 1.298-1.198h-.25c.66 0 1.145.533 1.086 1.2l-.106 1.198h1.03c.554 0 1.003.446 1.003 1.004 0 .555-.455 1.004-1 1.004H22.8l-.25 2.808h1.037c.554 0 1.002.446 1.002 1.004 0 .554-.456 1.004-1.003 1.004h-1.214l-.144 1.627c-.06.662-.646 1.2-1.3 1.2h.25c-.658 0-1.144-.534-1.085-1.2l.144-1.627h-3.367z"/> + <path fill="#6B4FBB" d="M17.05 19.262l-.177 2.008H14.74l.177-2.008h2.134zm-1.707-4.816h2.135l-.178 2.008h-2.135l.178-2.008zm5.5 0h2.135l-.178 2.008h-2.135l.178-2.008zm1.708 4.816l-.177 2.008H20.24l.177-2.008h2.134z"/> + </g> + </g> +</svg> diff --git a/app/views/shared/icons/_i2p_step_1.svg b/app/views/shared/icons/_i2p_step_1.svg new file mode 100644 index 00000000000..9dedcd5291a --- /dev/null +++ b/app/views/shared/icons/_i2p_step_1.svg @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m45.688 18.854c-4.869-1.989-10.488-1.975-15.29-.001-2.413.979-4.597 2.414-6.493 4.268-1.836 1.8-3.33 3.985-4.346 6.381-1.013 2.38-1.525 4.916-1.525 7.537 0 2.066.33 4.118.983 6.104.469 1.388 1.089 2.706 1.83 3.937-1.275 1.101-2.086 2.725-2.086 4.538 0 3.309 2.691 6 6 6s6-2.691 6-6-2.691-6-6-6c-.779 0-1.522.154-2.205.425-.665-1.105-1.221-2.289-1.642-3.533-.585-1.776-.881-3.618-.881-5.472 0-2.351.459-4.623 1.391-6.814.89-2.096 2.231-4.059 3.88-5.675 1.708-1.669 3.675-2.962 5.85-3.845 4.329-1.778 9.392-1.79 13.78.002 2.17.881 4.137 2.175 5.843 3.84 3.39 3.34 5.257 7.776 5.257 12.493.002 1.86-.294 3.705-.878 5.481-.579 1.75-1.443 3.406-2.569 4.923-2.134 2.866-3.818 4.698-5.174 6.173-2.424 2.643-3.98 4.599-4.383 8.384h-10.815c-.553 0-1 .447-1 1s.447 1 1 1h11.739c.532 0 .971-.416.999-.947.19-3.645 1.345-5.263 3.934-8.09 1.385-1.506 3.107-3.381 5.304-6.331 1.254-1.688 2.218-3.535 2.864-5.489.651-1.98.98-4.04.979-6.109 0-5.256-2.078-10.198-5.856-13.92-1.897-1.851-4.081-3.287-6.49-4.265m-16.927 32.763c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4"/> + <path d="m40 74h-4c-.553 0-1 .447-1 1s.447 1 1 1h4c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m42 70h-8c-.553 0-1 .447-1 1s.447 1 1 1h8c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m38 10c.553 0 1-.447 1-1v-8c0-.553-.447-1-1-1s-1 .447-1 1v8c0 .553.447 1 1 1"/> + <path d="m20.828 15.828c.256 0 .512-.098.707-.293.391-.391.391-1.023 0-1.414l-5.656-5.656c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l5.656 5.656c.195.195.451.293.707.293"/> + <path d="m10 33h-8c-.553 0-1 .447-1 1s.447 1 1 1h8c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m60.12 8.465l-5.656 5.656c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293s.512-.098.707-.293l5.656-5.656c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0"/> + <path d="m74 33h-8c-.553 0-1 .447-1 1s.447 1 1 1h8c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m43 66h-10c-.553 0-1 .447-1 1s.447 1 1 1h10c.553 0 1-.447 1-1s-.447-1-1-1"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_10.svg b/app/views/shared/icons/_i2p_step_10.svg new file mode 100644 index 00000000000..dd6fd1457ff --- /dev/null +++ b/app/views/shared/icons/_i2p_step_10.svg @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m5 43c0 .553.447 1 1 1s1-.447 1-1v-4h4c.553 0 1-.447 1-1s-.447-1-1-1h-4v-4c0-.553-.447-1-1-1s-1 .447-1 1v4h-4c-.553 0-1 .447-1 1s.447 1 1 1h4v4"/> + <path d="m75 37h-4v-4c0-.553-.447-1-1-1s-1 .447-1 1v4h-4c-.553 0-1 .447-1 1s.447 1 1 1h4v4c0 .553.447 1 1 1s1-.447 1-1v-4h4c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m21 38c0 .345.178.665.47.848l8 5c.165.103.348.152.529.152.333 0 .659-.166.849-.47.293-.469.15-1.086-.317-1.378l-6.644-4.152 6.644-4.152c.468-.292.61-.909.317-1.378s-.908-.611-1.378-.317l-8 5c-.292.182-.47.502-.47.847"/> + <path d="m55 38c0-.345-.178-.665-.47-.848l-8-5c-.469-.294-1.086-.151-1.378.317-.293.469-.15 1.086.317 1.378l6.644 4.153-6.644 4.152c-.468.292-.61.909-.317 1.378.189.304.516.47.849.47.181 0 .364-.049.529-.152l8-5c.292-.183.47-.503.47-.848"/> + <path d="m41.803 26.05c-.525-.168-1.089.124-1.256.65l-7 22c-.167.525.124 1.088.65 1.256.101.032.202.047.303.047.424 0 .817-.271.953-.697l7-22c.167-.526-.124-1.088-.65-1.256"/> + <path d="m62 7c3.859 0 7 3.141 7 7v11c0 .553.447 1 1 1s1-.447 1-1v-11c0-4.963-4.04-9-9-9h-16.09c-.479-2.833-2.943-5-5.91-5-3.309 0-6 2.691-6 6s2.691 6 6 6c2.967 0 5.431-2.167 5.91-5h16.09m-22 3c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4"/> + <path d="m6 26c.553 0 1-.447 1-1v-11c0-3.859 3.141-7 7-7h11.09l-3.293 3.293c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293s.512-.098.707-.293l5-5c.391-.391.391-1.023 0-1.414l-5-5c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l3.293 3.293h-11.09c-4.963 0-9 4.04-9 9v11c0 .553.447 1 1 1"/> + <path d="m36 64c-2.967 0-5.431 2.167-5.91 5h-16.09c-3.859 0-7-3.141-7-7v-11c0-.553-.447-1-1-1s-1 .447-1 1v11c0 4.963 4.04 9 9 9h16.09c.478 2.833 2.942 5 5.91 5 3.309 0 6-2.691 6-6s-2.691-6-6-6m0 10c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4"/> + <path d="m70 50c-.553 0-1 .447-1 1v11c0 3.859-3.141 7-7 7h-11.09l3.293-3.293c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0l-5 5c-.391.391-.391 1.023 0 1.414l5 5c.195.195.451.293.707.293s.512-.098.707-.293c.391-.391.391-1.023 0-1.414l-3.293-3.293h11.09c4.963 0 9-4.04 9-9v-11c0-.553-.447-1-1-1"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_2.svg b/app/views/shared/icons/_i2p_step_2.svg new file mode 100644 index 00000000000..b8805b90275 --- /dev/null +++ b/app/views/shared/icons/_i2p_step_2.svg @@ -0,0 +1,5 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m42.26 40.44c.558.073 1.045-.329 1.109-.877l2.625-22.444c.033-.283-.057-.567-.246-.781-.189-.214-.462-.336-.747-.336h-14c-.284 0-.555.121-.744.332-.19.212-.281.494-.25.776l3.454 31.575c-1.503 1.285-2.46 3.19-2.46 5.317 0 3.859 3.141 7 7 7s7-3.141 7-7-3.141-7-7-7c-.94 0-1.835.189-2.655.527l-3.23-29.527h11.761l-2.494 21.328c-.065.549.328 1.045.877 1.11m.741 13.562c0 2.757-2.243 5-5 5s-5-2.243-5-5 2.243-5 5-5 5 2.243 5 5"/> + <path d="M73.236,23.749c-0.207-0.513-0.796-0.76-1.302-0.552c-0.513,0.207-0.759,0.79-0.552,1.302 C73.119,28.787,74,33.329,74,38c0,19.851-16.149,36-36,36S2,57.851,2,38S18.149,2,38,2c7.6,0,14.83,2.332,20.965,6.74 C58.339,9.702,58,10.825,58,12c0,1.603,0.624,3.109,1.758,4.242C60.891,17.376,62.397,18,64,18c1.603,0,3.109-0.624,4.242-1.758 C69.376,15.109,70,13.603,70,12s-0.624-3.109-1.758-4.242C67.109,6.624,65.603,6,64,6c-1.346,0-2.622,0.445-3.668,1.259 C53.812,2.512,46.104,0,38,0C17.047,0,0,17.047,0,38s17.047,38,38,38s38-17.047,38-38C76,33.07,75.07,28.275,73.236,23.749z M64,8 c1.068,0,2.072,0.416,2.828,1.172S68,10.932,68,12s-0.416,2.072-1.172,2.828c-1.512,1.512-4.145,1.512-5.656,0 C60.416,14.072,60,13.068,60,12s0.416-2.072,1.172-2.828S62.932,8,64,8z"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_3.svg b/app/views/shared/icons/_i2p_step_3.svg new file mode 100644 index 00000000000..6c783ed8289 --- /dev/null +++ b/app/views/shared/icons/_i2p_step_3.svg @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m12 8c0-3.309-2.691-6-6-6s-6 2.691-6 6c0 2.967 2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909 0 3.309 2.691 6 6 6s6-2.691 6-6c0-2.967-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.479 5-2.943 5-5.91m-10 0c0-2.206 1.794-4 4-4s4 1.794 4 4-1.794 4-4 4-4-1.794-4-4m8 60c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4"/> + <path d="m21 6h54c.553 0 1-.447 1-1s-.447-1-1-1h-54c-.553 0-1 .447-1 1s.447 1 1 1"/> + <path d="m21 12h35c.553 0 1-.447 1-1s-.447-1-1-1h-35c-.553 0-1 .447-1 1s.447 1 1 1"/> + <path d="m75 24h-54c-.553 0-1 .447-1 1s.447 1 1 1h54c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m21 32h34c.553 0 1-.447 1-1s-.447-1-1-1h-34c-.553 0-1 .447-1 1s.447 1 1 1"/> + <path d="m75 44h-54c-.553 0-1 .447-1 1s.447 1 1 1h54c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m21 52h34c.553 0 1-.447 1-1s-.447-1-1-1h-34c-.553 0-1 .447-1 1s.447 1 1 1"/> + <path d="m75 64h-54c-.553 0-1 .447-1 1s.447 1 1 1h54c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m55 70h-34c-.553 0-1 .447-1 1s.447 1 1 1h34c.553 0 1-.447 1-1s-.447-1-1-1"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_4.svg b/app/views/shared/icons/_i2p_step_4.svg new file mode 100644 index 00000000000..af804c838e0 --- /dev/null +++ b/app/views/shared/icons/_i2p_step_4.svg @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m67.7 10h-6.751c-.507-5.598-5.221-10-10.949-10-6.06 0-11 4.935-11 11s4.935 11 11 11c5.728 0 10.442-4.402 10.949-10h6.751c1.269 0 2.3.987 2.3 2.2v57.6c0 1.213-1.031 2.2-2.3 2.2h-59.4c-1.269 0-2.3-.987-2.3-2.2v-57.6c0-1.213 1.031-2.2 2.3-2.2h15.15c.553 0 1-.447 1-1s-.447-1-1-1h-15.15c-2.371 0-4.3 1.884-4.3 4.2v57.6c0 2.316 1.929 4.2 4.3 4.2h59.4c2.371 0 4.3-1.884 4.3-4.2v-57.6c0-2.316-1.929-4.2-4.3-4.2m-17.7 10c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/> + <path d="m21.293 29.29c-.391.391-.391 1.023 0 1.414l12.975 12.975-12.975 12.974c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293s.512-.098.707-.293l13.682-13.682c.391-.391.391-1.023 0-1.414l-13.682-13.681c-.391-.391-1.023-.391-1.414 0"/> + <path d="m54 59c.553 0 1-.447 1-1s-.447-1-1-1h-12c-.553 0-1 .447-1 1s.447 1 1 1h12"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_5.svg b/app/views/shared/icons/_i2p_step_5.svg new file mode 100644 index 00000000000..e54f707019e --- /dev/null +++ b/app/views/shared/icons/_i2p_step_5.svg @@ -0,0 +1,5 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m48.949 37c-.507-5.598-5.221-10-10.949-10s-10.442 4.402-10.949 10h-13.05c-.553 0-1 .447-1 1s.447 1 1 1h13.05c.507 5.598 5.221 10 10.949 10s10.442-4.402 10.949-10h12.24c.553 0 1-.447 1-1s-.447-1-1-1h-12.24m-10.949 10c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/> + <path d="M73.236,23.749c-0.207-0.513-0.797-0.76-1.302-0.552c-0.513,0.207-0.759,0.79-0.552,1.302 C73.119,28.787,74,33.329,74,38c0,19.851-16.149,36-36,36S2,57.851,2,38S18.149,2,38,2c7.6,0,14.83,2.332,20.965,6.74 C58.339,9.702,58,10.825,58,12c0,1.603,0.624,3.109,1.758,4.242C60.891,17.376,62.397,18,64,18c1.603,0,3.109-0.624,4.242-1.758 C69.376,15.109,70,13.603,70,12s-0.624-3.109-1.758-4.242C67.109,6.624,65.603,6,64,6c-1.346,0-2.622,0.445-3.668,1.259 C53.812,2.512,46.104,0,38,0C17.047,0,0,17.047,0,38s17.047,38,38,38s38-17.047,38-38C76,33.07,75.07,28.275,73.236,23.749z M64,8 c1.068,0,2.072,0.416,2.828,1.172S68,10.932,68,12s-0.416,2.072-1.172,2.828c-1.512,1.512-4.145,1.512-5.656,0 C60.416,14.072,60,13.068,60,12s0.416-2.072,1.172-2.828S62.932,8,64,8z"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_6.svg b/app/views/shared/icons/_i2p_step_6.svg new file mode 100644 index 00000000000..c57baccc06b --- /dev/null +++ b/app/views/shared/icons/_i2p_step_6.svg @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m14.267 7.32l-4.896 5.277-1.702-1.533c-.409-.369-1.043-.338-1.412.074-.369.41-.337 1.042.074 1.412l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062.114.044.235.066.356.066.135 0 .27-.028.396-.082.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6c.375-.404.352-1.037-.054-1.413-.405-.377-1.036-.353-1.412.053"/> + <path d="m31 9h44c.553 0 1-.447 1-1s-.447-1-1-1h-44c-.553 0-1 .447-1 1s.447 1 1 1"/> + <path d="m31 15h24c.553 0 1-.447 1-1s-.447-1-1-1h-24c-.553 0-1 .447-1 1s.447 1 1 1"/> + <path d="m11 0c-6.07 0-11 4.935-11 11s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/> + <path d="m14.267 34.32l-4.896 5.277-1.702-1.533c-.409-.368-1.043-.338-1.412.074-.369.41-.337 1.042.074 1.412l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062.114.044.235.066.356.066.135 0 .27-.028.396-.082.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6c.375-.404.352-1.037-.054-1.413-.405-.377-1.036-.353-1.412.053"/> + <path d="m75 34h-44c-.553 0-1 .447-1 1s.447 1 1 1h44c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m31 42h24c.553 0 1-.447 1-1s-.447-1-1-1h-24c-.553 0-1 .447-1 1s.447 1 1 1"/> + <path d="m11 27c-6.07 0-11 4.935-11 11s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/> + <path d="m14.267 61.32l-4.896 5.277-1.702-1.533c-.409-.368-1.043-.338-1.412.074-.369.41-.337 1.042.074 1.412l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062.114.044.235.066.356.066.135 0 .27-.028.396-.082.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6c.375-.404.352-1.037-.054-1.413-.405-.377-1.036-.353-1.412.053"/> + <path d="m11 54c-6.07 0-11 4.935-11 11s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/> + <path d="m75 61h-44c-.553 0-1 .447-1 1s.447 1 1 1h44c.553 0 1-.447 1-1s-.447-1-1-1"/> + <path d="m55 67h-24c-.553 0-1 .447-1 1s.447 1 1 1h24c.553 0 1-.447 1-1s-.447-1-1-1"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_7.svg b/app/views/shared/icons/_i2p_step_7.svg new file mode 100644 index 00000000000..e9083de3afa --- /dev/null +++ b/app/views/shared/icons/_i2p_step_7.svg @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="M73.236,23.749c-0.208-0.513-0.798-0.76-1.302-0.552c-0.513,0.207-0.759,0.79-0.552,1.302 C73.119,28.787,74,33.329,74,38c0,19.851-16.149,36-36,36S2,57.851,2,38S18.149,2,38,2c7.6,0,14.83,2.332,20.965,6.74 C58.339,9.702,58,10.825,58,12c0,1.603,0.624,3.109,1.758,4.242C60.891,17.376,62.397,18,64,18c1.603,0,3.109-0.624,4.242-1.758 C69.376,15.109,70,13.603,70,12s-0.624-3.109-1.758-4.242C67.109,6.624,65.603,6,64,6c-1.346,0-2.622,0.445-3.668,1.259 C53.812,2.512,46.104,0,38,0C17.047,0,0,17.047,0,38s17.047,38,38,38s38-17.047,38-38C76,33.07,75.07,28.275,73.236,23.749z M64,8 c1.068,0,2.072,0.416,2.828,1.172S68,10.932,68,12s-0.416,2.072-1.172,2.828c-1.512,1.512-4.145,1.512-5.656,0 C60.416,14.072,60,13.068,60,12s0.416-2.072,1.172-2.828S62.932,8,64,8z"/> + <path d="m27.19 32.17c-.277-.479-.89-.643-1.366-.364l-12.654 7.326c-.309.179-.499.509-.499.865s.19.687.499.865l12.654 7.326c.157.092.33.135.5.135.345 0 .681-.179.866-.499.277-.478.113-1.09-.364-1.366l-11.159-6.461 11.159-6.461c.478-.276.642-.889.364-1.366"/> + <path d="m48.808 47.827c.186.32.521.499.866.499.17 0 .343-.043.5-.135l12.654-7.326c.309-.179.499-.509.499-.865s-.19-.687-.499-.865l-12.654-7.326c-.478-.278-1.09-.114-1.366.364-.277.478-.113 1.09.364 1.366l11.159 6.461-11.159 6.461c-.478.276-.642.889-.364 1.366"/> + <path d="m42.71 23.06l-11.312 33.23c-.179.522.102 1.091.624 1.269.106.037.216.054.322.054.416 0 .805-.262.946-.678l11.312-33.23c.179-.522-.102-1.091-.624-1.269-.523-.181-1.089.101-1.268.624"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_8.svg b/app/views/shared/icons/_i2p_step_8.svg new file mode 100644 index 00000000000..62676b0e12e --- /dev/null +++ b/app/views/shared/icons/_i2p_step_8.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m62.44 54.765l-9.912-11.09c.315-3.881.481-7.241.508-10.271-.029-13.871-3.789-23.05-13.413-32.746-.855-.859-2.411-.828-3.294.059-7.594 7.65-11.139 13.934-12.575 22.3-1.776.062-3.437.776-4.699 2.039-1.321 1.321-2.05 3.079-2.05 4.949s.729 3.628 2.051 4.949c1.321 1.322 3.079 2.051 4.949 2.051s3.628-.729 4.949-2.051c1.322-1.321 2.051-3.079 2.051-4.949 0-1.869-.729-3.627-2.051-4.949-.9-.9-2-1.517-3.205-1.824 1.373-7.859 4.764-13.818 11.999-21.11.128-.13.356-.158.456-.059 9.207 9.274 12.805 18.06 12.832 31.33-.026 3.079-.202 6.527-.536 10.54-.023.273.067.545.25.749l10.166 11.379c.062.076.109.23.093.32l-4.547 17.407c-.004.015-.009.036-.079.106-.062.063-.155.1-.2.106l-3.577.002c-.144-.009-.265-.077-.309-.153l-5.425-10.328c-.173-.329-.515-.535-.886-.535h-15.962c-.371 0-.713.206-.886.535l-5.407 10.303-.069.072c-.07.07-.165.105-.199.105l-3.588.001c-.179-.009-.304-.123-.33-.227l-4.531-17.338c-.029-.146.019-.301.049-.34l10.197-11.415c.367-.412.332-1.044-.08-1.412-.411-.366-1.042-.333-1.412.08l-10.229 11.453c-.448.554-.63 1.312-.474 2.084l4.544 17.396c.253.963 1.146 1.669 2.218 1.719h3.636c.581 0 1.187-.261 1.615-.693.114-.114.286-.286.406-.528l5.144-9.793h14.754l5.16 9.822c.396.697 1.124 1.143 2.01 1.192l3.712-.003c.604-.046 1.137-.285 1.544-.694.313-.316.504-.646.598-1.022l4.557-17.451c.143-.718-.039-1.476-.518-2.066m-33.435-24.765c0 1.335-.521 2.591-1.465 3.535s-2.2 1.465-3.535 1.465-2.591-.521-3.535-1.465-1.465-2.2-1.465-3.535.521-2.591 1.465-3.535 2.2-1.465 3.535-1.465 2.591.521 3.535 1.465 1.465 2.2 1.465 3.535"/> +</svg> diff --git a/app/views/shared/icons/_i2p_step_9.svg b/app/views/shared/icons/_i2p_step_9.svg new file mode 100644 index 00000000000..e4285a14425 --- /dev/null +++ b/app/views/shared/icons/_i2p_step_9.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76"> + <path d="m68 67c-1.725 0-3.36.541-4.723 1.545-2.298-4.02-6.592-6.545-11.277-6.545-2.734 0-5.359.853-7.555 2.43l-2.286-15.43h1.228l3.829 7.645c.339.598.962.979 1.724 1.022l2.812-.003c.507-.039.974-.25 1.316-.595.264-.266.433-.559.514-.882l3.433-13.145c.12-.611-.037-1.258-.449-1.763l-7.385-8.268c.231-2.875.354-5.376.374-7.641-.023-10.507-2.871-17.462-10.162-24.806-.737-.742-2.072-.715-2.829.044-5.617 5.659-8.309 10.336-9.446 16.463-1.267.186-2.438.764-3.36 1.686-1.134 1.134-1.758 2.64-1.758 4.243s.624 3.109 1.758 4.242c1.133 1.134 2.639 1.758 4.242 1.758s3.109-.624 4.242-1.758c1.134-1.133 1.758-2.639 1.758-4.242s-.624-3.109-1.758-4.242c-.858-.859-1.932-1.424-3.098-1.648 1.095-5.538 3.637-9.855 8.83-15.14 6.874 6.924 9.561 13.485 9.581 23.392-.021 2.316-.151 4.903-.402 7.91-.023.273.067.544.25.749l7.663 8.572-3.391 13.07-2.695.036-4.081-8.15c-.17-.339-.516-.553-.895-.553h-12.01c-.379 0-.725.214-.895.553l-4.04 8.114-2.707.015-3.427-13.07 7.671-8.588c.367-.412.332-1.044-.08-1.412-.411-.366-1.043-.333-1.412.08l-7.7 8.623c-.383.47-.54 1.116-.406 1.787l3.419 13.08c.216.829.98 1.438 1.907 1.48h2.735c.508 0 1.016-.218 1.391-.595.091-.09.242-.241.358-.475l3.804-7.597h1.228l-2.286 15.43c-2.196-1.577-4.821-2.43-7.555-2.43-4.685 0-8.979 2.53-11.277 6.545-1.363-1-2.998-1.545-4.723-1.545-4.411 0-8 3.589-8 8 0 .553.447 1 1 1h74c.553 0 1-.447 1-1 0-4.411-3.589-8-8-8m-36-44c0 1.068-.416 2.072-1.172 2.828-1.512 1.512-4.145 1.512-5.656 0-.756-.756-1.172-1.76-1.172-2.828s.416-2.072 1.172-2.828 1.76-1.172 2.828-1.172 2.072.416 2.828 1.172 1.172 1.76 1.172 2.828m-29.917 51c.478-2.834 2.949-5 5.917-5 1.638 0 3.17.652 4.313 1.836.231.24.562.35.895.29.327-.058.604-.274.739-.579 1.765-3.977 5.711-6.547 10.05-6.547 2.836 0 5.532 1.085 7.593 3.055.271.258.665.345 1.016.224.354-.122.61-.43.665-.8l2.588-17.479h4.275l2.589 17.479c.055.37.312.678.665.8s.745.035 1.016-.224c2.061-1.97 4.757-3.055 7.593-3.055 4.343 0 8.288 2.57 10.05 6.547.135.305.412.521.739.579.329.059.663-.051.895-.29 1.143-1.184 2.675-1.836 4.313-1.836 2.968 0 5.439 2.166 5.917 5h-71.834"/> +</svg> diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c239253c8d5..f246bd7a586 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -100,7 +100,7 @@ Snippets %div{ class: container_class } - - if @user == current_user && show_user_callout? + - if @user == current_user && show_callout?('user_callout_dismissed') = render 'shared/user_callout' .tab-content #activity.tab-pane diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index e17add7421f..bf009dfab0f 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -11,15 +11,6 @@ class BuildSuccessWorker private def create_deployment(build) - service = CreateDeploymentService.new( - build.project, build.user, - environment: build.environment, - sha: build.sha, - ref: build.ref, - tag: build.tag, - options: build.options.to_h[:environment], - variables: build.variables) - - service.execute(build) + CreateDeploymentService.new(build).execute end end diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb index 2f02235b0ac..0a55aab63fd 100644 --- a/app/workers/gitlab_usage_ping_worker.rb +++ b/app/workers/gitlab_usage_ping_worker.rb @@ -3,29 +3,17 @@ class GitlabUsagePingWorker include Sidekiq::Worker include CronjobQueue - include HTTParty def perform - return unless current_application_settings.usage_ping_enabled - # Multiple Sidekiq workers could run this. We should only do this at most once a day. return unless try_obtain_lease - begin - HTTParty.post(url, - body: Gitlab::UsageData.to_json(force_refresh: true), - headers: { 'Content-type' => 'application/json' } - ) - rescue HTTParty::Error => e - Rails.logger.info "Unable to contact GitLab, Inc.: #{e}" - end + SubmitUsagePingService.new.execute end + private + def try_obtain_lease Gitlab::ExclusiveLease.new('gitlab_usage_ping_worker:ping', timeout: LEASE_TIMEOUT).try_obtain end - - def url - 'https://version.gitlab.com/usage_data' - end end diff --git a/changelogs/unreleased/25680-CI_ENVIRONMENT_URL.yml b/changelogs/unreleased/25680-CI_ENVIRONMENT_URL.yml new file mode 100644 index 00000000000..af9fe3b5041 --- /dev/null +++ b/changelogs/unreleased/25680-CI_ENVIRONMENT_URL.yml @@ -0,0 +1,4 @@ +--- +title: Add $CI_ENVIRONMENT_URL to predefined variables for pipelines +merge_request: 11695 +author: diff --git a/changelogs/unreleased/27614-improve-instant-comments-exp.yml b/changelogs/unreleased/27614-improve-instant-comments-exp.yml new file mode 100644 index 00000000000..4db676801f1 --- /dev/null +++ b/changelogs/unreleased/27614-improve-instant-comments-exp.yml @@ -0,0 +1,4 @@ +--- +title: Improve user experience around slash commands in instant comments +merge_request: 11612 +author: diff --git a/changelogs/unreleased/30469-convdev-index.yml b/changelogs/unreleased/30469-convdev-index.yml new file mode 100644 index 00000000000..0bdd9c4a699 --- /dev/null +++ b/changelogs/unreleased/30469-convdev-index.yml @@ -0,0 +1,4 @@ +--- +title: Add ConvDev Index page to admin area +merge_request: 11377 +author: diff --git a/changelogs/unreleased/31061-26135-ci-project-slug-enviroment-variables.yml b/changelogs/unreleased/31061-26135-ci-project-slug-enviroment-variables.yml new file mode 100644 index 00000000000..e71910dbd67 --- /dev/null +++ b/changelogs/unreleased/31061-26135-ci-project-slug-enviroment-variables.yml @@ -0,0 +1,4 @@ +--- +title: Add slugify project path to CI enviroment variables +merge_request: 11838 +author: Ivan Chernov diff --git a/changelogs/unreleased/31983-increase-merge-request-diff-file-size-limit-for-default-toggle-opening.yml b/changelogs/unreleased/31983-increase-merge-request-diff-file-size-limit-for-default-toggle-opening.yml new file mode 100644 index 00000000000..f61aa0a6b6e --- /dev/null +++ b/changelogs/unreleased/31983-increase-merge-request-diff-file-size-limit-for-default-toggle-opening.yml @@ -0,0 +1,4 @@ +--- +title: Increase individual diff collapse limit to 100 KB, and render limit to 200 KB +merge_request: +author: diff --git a/changelogs/unreleased/32992-consider-using-zopfli-over-standard-gzip-compression-for-webpack-assets.yml b/changelogs/unreleased/32992-consider-using-zopfli-over-standard-gzip-compression-for-webpack-assets.yml new file mode 100644 index 00000000000..93037d6181e --- /dev/null +++ b/changelogs/unreleased/32992-consider-using-zopfli-over-standard-gzip-compression-for-webpack-assets.yml @@ -0,0 +1,4 @@ +--- +title: Use zopfli compression for frontend assets +merge_request: 11798 +author: diff --git a/changelogs/unreleased/33154-permissions-for-project-labels-and-group-labels.yml b/changelogs/unreleased/33154-permissions-for-project-labels-and-group-labels.yml new file mode 100644 index 00000000000..3b98525167d --- /dev/null +++ b/changelogs/unreleased/33154-permissions-for-project-labels-and-group-labels.yml @@ -0,0 +1,4 @@ +--- +title: Allow group reporters to manage group labels +merge_request: +author: diff --git a/changelogs/unreleased/dz-fix-submodule-subgroup.yml b/changelogs/unreleased/dz-fix-submodule-subgroup.yml new file mode 100644 index 00000000000..20c7c9ce657 --- /dev/null +++ b/changelogs/unreleased/dz-fix-submodule-subgroup.yml @@ -0,0 +1,4 @@ +--- +title: Fix submodule link to then project under subgroup +merge_request: 11906 +author: diff --git a/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml b/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml new file mode 100644 index 00000000000..161bce45601 --- /dev/null +++ b/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml @@ -0,0 +1,4 @@ +--- +title: Fix LFS timeouts when trying to save large files +merge_request: +author: diff --git a/changelogs/unreleased/zj-job-view-goes-real-time.yml b/changelogs/unreleased/zj-job-view-goes-real-time.yml new file mode 100644 index 00000000000..376c9dfa65f --- /dev/null +++ b/changelogs/unreleased/zj-job-view-goes-real-time.yml @@ -0,0 +1,4 @@ +--- +title: Job details page update real time +merge_request: 11651 +author: diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb new file mode 100644 index 00000000000..a78711fe599 --- /dev/null +++ b/config/initializers/forbid_sidekiq_in_transactions.rb @@ -0,0 +1,49 @@ +module Sidekiq + module Worker + mattr_accessor :skip_transaction_check + self.skip_transaction_check = false + + def self.skipping_transaction_check(&block) + skip_transaction_check = self.skip_transaction_check + self.skip_transaction_check = true + yield + ensure + self.skip_transaction_check = skip_transaction_check + end + + module ClassMethods + module NoSchedulingFromTransactions + NESTING = ::Rails.env.test? ? 1 : 0 + + %i(perform_async perform_at perform_in).each do |name| + define_method(name) do |*args| + return super(*args) if Sidekiq::Worker.skip_transaction_check + return super(*args) unless ActiveRecord::Base.connection.open_transactions > NESTING + + raise <<-MSG.strip_heredoc + `#{self}.#{name}` cannot be called inside a transaction as this can lead to + race conditions when the worker runs before the transaction is committed and + tries to access a model that has not been saved yet. + + Use an `after_commit` hook, or include `AfterCommitQueue` and use a `run_after_commit` block instead. + MSG + end + end + end + + prepend NoSchedulingFromTransactions + end + end +end + +module ActiveRecord + class Base + module SkipTransactionCheckAfterCommit + def committed!(*) + Sidekiq::Worker.skipping_transaction_check { super } + end + end + + prepend SkipTransactionCheckAfterCommit + end +end diff --git a/config/routes/admin.rb b/config/routes/admin.rb index ccfd85aed63..c7b639b7b3c 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -72,6 +72,8 @@ namespace :admin do resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } + get 'conversational_development_index' => 'conversational_development_index#show' + resources :projects, only: [:index] scope(path: 'projects/*namespace_id', diff --git a/config/webpack.config.js b/config/webpack.config.js index c99298239b2..61f1eaaacd1 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -16,6 +16,7 @@ var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var WEBPACK_REPORT = process.env.WEBPACK_REPORT; +var NO_COMPRESSION = process.env.NO_COMPRESSION; var config = { // because sqljs requires fs. @@ -221,11 +222,18 @@ if (IS_PRODUCTION) { }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } - }), - new CompressionPlugin({ - asset: '[path].gz[query]', }) ); + + // zopfli requires a lot of compute time and is disabled in CI + if (!NO_COMPRESSION) { + config.plugins.push( + new CompressionPlugin({ + asset: '[path].gz[query]', + algorithm: 'zopfli', + }) + ); + } } if (IS_DEV_SERVER) { diff --git a/db/fixtures/development/11_keys.rb b/db/fixtures/development/11_keys.rb index 51e22137d6f..c405ecfdaf3 100644 --- a/db/fixtures/development/11_keys.rb +++ b/db/fixtures/development/11_keys.rb @@ -1,17 +1,26 @@ require './spec/support/sidekiq' + # Creating keys runs a gitlab-shell worker. Since we may not have the right # gitlab-shell path set (yet) we need to disable this for these fixtures. Sidekiq::Testing.disable! do Gitlab::Seeder.quiet do + # We want to run `add_to_shell` immediately instead of after the commit, so + # that it falls under `Sidekiq::Testing.disable!`. + Key.skip_callback(:commit, :after, :add_to_shell) + User.first(10).each do |user| key = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt#{user.id + 100}6k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" - user.keys.create( + key = user.keys.create( title: "Sample key #{user.id}", key: key ) + Sidekiq::Worker.skipping_transaction_check do + key.add_to_shell + end + print '.' end end diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 68767f0e585..5de5339b70e 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -112,6 +112,10 @@ class Gitlab::Seeder::Pipelines setup_artifacts(build) setup_build_log(build) + + build.project.environments. + find_or_create_by(name: build.expanded_environment_name) + build.save end end diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 75457b2d369..7c1d758dada 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -212,12 +212,9 @@ class Gitlab::Seeder::CycleAnalytics merge_requests.each do |merge_request| Timecop.travel 12.hours.from_now - CreateDeploymentService.new(merge_request.project, @user, { - environment: 'production', - ref: 'master', - tag: false, - sha: @project.repository.commit('master').sha - }).execute + job = merge_request.head_pipeline.builds.where.not(environment: nil).last + + CreateDeploymentService.new(job).execute end end end diff --git a/db/fixtures/development/21_conversational_development_index_metrics.rb b/db/fixtures/development/21_conversational_development_index_metrics.rb new file mode 100644 index 00000000000..4cd0a82ed1a --- /dev/null +++ b/db/fixtures/development/21_conversational_development_index_metrics.rb @@ -0,0 +1,40 @@ +Gitlab::Seeder.quiet do + conversational_development_index_metric = ConversationalDevelopmentIndex::Metric.new( + leader_issues: 10.2, + instance_issues: 3.2, + + leader_notes: 25.3, + instance_notes: 23.2, + + leader_milestones: 16.2, + instance_milestones: 5.5, + + leader_boards: 5.2, + instance_boards: 3.2, + + leader_merge_requests: 5.2, + instance_merge_requests: 3.2, + + leader_ci_pipelines: 25.1, + instance_ci_pipelines: 21.3, + + leader_environments: 3.3, + instance_environments: 2.2, + + leader_deployments: 41.3, + instance_deployments: 15.2, + + leader_projects_prometheus_active: 0.31, + instance_projects_prometheus_active: 0.30, + + leader_service_desk_issues: 15.8, + instance_service_desk_issues: 15.1 + ) + + if conversational_development_index_metric.save + print '.' + else + puts conversational_development_index_metric.errors.full_messages + print 'F' + end +end diff --git a/db/migrate/20170523121229_create_conversational_development_index_metrics.rb b/db/migrate/20170523121229_create_conversational_development_index_metrics.rb new file mode 100644 index 00000000000..9f9ec526055 --- /dev/null +++ b/db/migrate/20170523121229_create_conversational_development_index_metrics.rb @@ -0,0 +1,39 @@ +class CreateConversationalDevelopmentIndexMetrics < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :conversational_development_index_metrics do |t| + t.float :leader_issues, null: false + t.float :instance_issues, null: false + + t.float :leader_notes, null: false + t.float :instance_notes, null: false + + t.float :leader_milestones, null: false + t.float :instance_milestones, null: false + + t.float :leader_boards, null: false + t.float :instance_boards, null: false + + t.float :leader_merge_requests, null: false + t.float :instance_merge_requests, null: false + + t.float :leader_ci_pipelines, null: false + t.float :instance_ci_pipelines, null: false + + t.float :leader_environments, null: false + t.float :instance_environments, null: false + + t.float :leader_deployments, null: false + t.float :instance_deployments, null: false + + t.float :leader_projects_prometheus_active, null: false + t.float :instance_projects_prometheus_active, null: false + + t.float :leader_service_desk_issues, null: false + t.float :instance_service_desk_issues, null: false + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fa1c5dc15c4..0496ce2ced3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -371,6 +371,31 @@ ActiveRecord::Schema.define(version: 20170525174156) do add_index "container_repositories", ["project_id", "name"], name: "index_container_repositories_on_project_id_and_name", unique: true, using: :btree add_index "container_repositories", ["project_id"], name: "index_container_repositories_on_project_id", using: :btree + create_table "conversational_development_index_metrics", force: :cascade do |t| + t.float "leader_issues", null: false + t.float "instance_issues", null: false + t.float "leader_notes", null: false + t.float "instance_notes", null: false + t.float "leader_milestones", null: false + t.float "instance_milestones", null: false + t.float "leader_boards", null: false + t.float "instance_boards", null: false + t.float "leader_merge_requests", null: false + t.float "instance_merge_requests", null: false + t.float "leader_ci_pipelines", null: false + t.float "instance_ci_pipelines", null: false + t.float "leader_environments", null: false + t.float "instance_environments", null: false + t.float "leader_deployments", null: false + t.float "instance_deployments", null: false + t.float "leader_projects_prometheus_active", null: false + t.float "instance_projects_prometheus_active", null: false + t.float "leader_service_desk_issues", null: false + t.float "instance_service_desk_issues", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false @@ -1446,8 +1471,8 @@ ActiveRecord::Schema.define(version: 20170525174156) do t.string "token" t.boolean "pipeline_events", default: false, null: false t.boolean "confidential_issues_events", default: false, null: false - t.boolean "job_events", default: false, null: false t.boolean "repository_update_events", default: false, null: false + t.boolean "job_events", default: false, null: false end add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 169e0fbae3d..3393030210e 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -94,6 +94,12 @@ the name given in `.gitlab-ci.yml` (with any variables expanded), while the second is a "cleaned-up" version of the name, suitable for use in URLs, DNS, etc. +>**Note:** +Starting with GitLab 9.3, the environment URL is exposed to the Runner via +`$CI_ENVIRONMENT_URL`. The URL would be expanded from `.gitlab-ci.yml`, or if +the URL was not defined there, the external URL from the environment would be +used. + To sum up, with the above `.gitlab-ci.yml` we have achieved that: - All branches will run the `test` and `build` jobs. diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 76ad7c564a3..56ff245f9f9 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -43,6 +43,7 @@ future GitLab releases.** | **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | | **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job | | **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | +| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job | | **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally | | **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started | | **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | @@ -56,9 +57,10 @@ future GitLab releases.** | **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run | | **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally | -| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built | +| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) | | **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built | | **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | +| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | | **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | @@ -122,7 +124,7 @@ global defined variables in your job, define an empty array: ```yaml job_name: - variables: [] + variables: {} ``` You are able to use other variables inside your variable definition (or escape them with `$$`): diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index fab5d14ac54..2c9aa437932 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -438,7 +438,7 @@ in your job, define an empty array: ```yaml job_name: - variables: [] + variables: {} ``` Job variables priority is defined in the [variables documentation][variables]. diff --git a/lib/feature.rb b/lib/feature.rb index 2e2b343f82c..5650a1c1334 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -27,6 +27,18 @@ class Feature all.map(&:name).include?(feature.name) end + def enabled?(key) + get(key).enabled? + end + + def enable(key) + get(key).enable + end + + def disable(key) + get(key).disable + end + private def flipper diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index 2f9d8bfc266..ca49eda51fb 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -9,8 +9,8 @@ module Gitlab # - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route # - Ending in `issues/id`/realtime_changes` for the `issue_title` route USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes - commit pipelines merge_requests new - environments].freeze + commit pipelines merge_requests builds + new environments].freeze RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape))) @@ -44,6 +44,10 @@ module Gitlab 'project_pipeline' ), Gitlab::EtagCaching::Router::Route.new( + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/builds/\d+\.json\z), + 'project_build' + ), + Gitlab::EtagCaching::Router::Route.new( %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z), 'environments' ) diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 7e21994a084..8926aa19925 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -20,13 +20,25 @@ module Gitlab # We need this accessor because of `to_hash` and `init_from_hash` attr_accessor :too_large - # The maximum size of a diff to display. - SIZE_LIMIT = 100.kilobytes + class << self + # The maximum size of a diff to display. + def size_limit + if Feature.enabled?('gitlab_git_diff_size_limit_increase') + 200.kilobytes + else + 100.kilobytes + end + end - # The maximum size before a diff is collapsed. - COLLAPSE_LIMIT = 10.kilobytes + # The maximum size before a diff is collapsed. + def collapse_limit + if Feature.enabled?('gitlab_git_diff_size_limit_increase') + 100.kilobytes + else + 10.kilobytes + end + end - class << self def between(repo, head, base, options = {}, *paths) straight = options.delete(:straight) || false @@ -231,7 +243,7 @@ module Gitlab def too_large? if @too_large.nil? - @too_large = @diff.bytesize >= SIZE_LIMIT + @too_large = @diff.bytesize >= self.class.size_limit else @too_large end @@ -246,7 +258,7 @@ module Gitlab def collapsed? return @collapsed if defined?(@collapsed) - @collapsed = !expanded && @diff.bytesize >= COLLAPSE_LIMIT + @collapsed = !expanded && @diff.bytesize >= self.class.collapse_limit end def collapse! @@ -318,14 +330,14 @@ module Gitlab hunk.each_line do |line| size += line.content.bytesize - if size >= SIZE_LIMIT + if size >= self.class.size_limit too_large! return true end end end - if !expanded && size >= COLLAPSE_LIMIT + if !expanded && size >= self.class.collapse_limit collapse! return true end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 838bdae1445..7211acc53dc 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -101,26 +101,49 @@ describe Projects::JobsController do end describe 'GET show' do - context 'when build exists' do - let!(:build) { create(:ci_build, pipeline: pipeline) } + let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } - before do - get_show(id: build.id) + context 'when requesting HTML' do + context 'when build exists' do + before do + get_show(id: build.id) + end + + it 'has a build' do + expect(response).to have_http_status(:ok) + expect(assigns(:build).id).to eq(build.id) + end end - it 'has a build' do - expect(response).to have_http_status(:ok) - expect(assigns(:build).id).to eq(build.id) + context 'when build does not exist' do + before do + get_show(id: 1234) + end + + it 'renders not_found' do + expect(response).to have_http_status(:not_found) + end end end - context 'when build does not exist' do + context 'when requesting JSON' do + let(:merge_request) { create(:merge_request, source_project: project) } + before do - get_show(id: 1234) + project.add_developer(user) + sign_in(user) + + allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request) + + get_show(id: build.id, format: :json) end - it 'renders not_found' do - expect(response).to have_http_status(:not_found) + it 'exposes needed information' do + expect(response).to have_http_status(:ok) + expect(json_response['raw_path']).to match(/builds\/\d+\/raw\z/) + expect(json_response.dig('merge_request', 'path')).to match(/merge_requests\/\d+\z/) + expect(json_response['new_issue_path']) + .to include('/issues/new') end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index f5e99fdf00b..0bb5a86d9b9 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -64,7 +64,8 @@ FactoryGirl.define do trait :teardown_environment do environment 'staging' options environment: { name: 'staging', - action: 'stop' } + action: 'stop', + url: 'http://staging.example.com/$CI_JOB_NAME' } end trait :allowed_to_fail do diff --git a/spec/factories/conversational_development_index_metrics.rb b/spec/factories/conversational_development_index_metrics.rb new file mode 100644 index 00000000000..a5412629195 --- /dev/null +++ b/spec/factories/conversational_development_index_metrics.rb @@ -0,0 +1,33 @@ +FactoryGirl.define do + factory :conversational_development_index_metric, class: ConversationalDevelopmentIndex::Metric do + leader_issues 9.256 + instance_issues 1.234 + + leader_notes 30.33333 + instance_notes 28.123 + + leader_milestones 16.2456 + instance_milestones 1.234 + + leader_boards 5.2123 + instance_boards 3.254 + + leader_merge_requests 1.2 + instance_merge_requests 0.6 + + leader_ci_pipelines 12.1234 + instance_ci_pipelines 2.344 + + leader_environments 3.3333 + instance_environments 2.2222 + + leader_deployments 1.200 + instance_deployments 0.771 + + leader_projects_prometheus_active 0.111 + instance_projects_prometheus_active 0.109 + + leader_service_desk_issues 15.891 + instance_service_desk_issues 13.345 + end +end diff --git a/spec/features/admin/admin_conversational_development_index_spec.rb b/spec/features/admin/admin_conversational_development_index_spec.rb new file mode 100644 index 00000000000..739ab907a29 --- /dev/null +++ b/spec/features/admin/admin_conversational_development_index_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'Admin Conversational Development Index' do + before do + login_as :admin + end + + context 'when usage ping is disabled' do + it 'shows empty state' do + stub_application_setting(usage_ping_enabled: false) + + visit admin_conversational_development_index_path + + expect(page).to have_content('Usage ping is not enabled') + end + end + + context 'when there is no data to display' do + it 'shows empty state' do + stub_application_setting(usage_ping_enabled: true) + + visit admin_conversational_development_index_path + + expect(page).to have_content('Data is still calculating') + end + end + + context 'when there is data to display' do + it 'shows numbers for each metric' do + stub_application_setting(usage_ping_enabled: true) + create(:conversational_development_index_metric) + + visit admin_conversational_development_index_path + + expect(page).to have_content( + 'Issues created per active user 1.2 You 9.3 Lead 13.3%' + ) + end + end +end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 0cb75538311..c4d5077e5e1 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -5,6 +5,11 @@ feature 'Expand and collapse diffs', js: true, feature: true do let(:project) { create(:project, :repository) } before do + # Set the limits to those when these specs were written, to avoid having to + # update the test repo every time we change them. + allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(100.kilobytes) + allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(10.kilobytes) + login_as :admin # Ensure that undiffable.md is in .gitattributes @@ -62,18 +67,6 @@ feature 'Expand and collapse diffs', js: true, feature: true do expect(small_diff).not_to have_selector('.nothing-here-block') end - it 'collapses large diffs by default' do - expect(large_diff).not_to have_selector('.code') - expect(large_diff).to have_selector('.nothing-here-block') - end - - it 'collapses large diffs for renamed files by default' do - expect(large_diff_renamed).not_to have_selector('.code') - expect(large_diff_renamed).to have_selector('.nothing-here-block') - expect(large_diff_renamed).to have_selector('.js-file-title .deletion') - expect(large_diff_renamed).to have_selector('.js-file-title .addition') - end - it 'shows non-renderable diffs as such immediately, regardless of their size' do expect(undiffable).not_to have_selector('.code') expect(undiffable).to have_selector('.nothing-here-block') diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 7f669565085..27e2d5d16f3 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -100,7 +100,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do context 'in Parallel view mode' do before do - click_link('conflicts', href: /\/conflicts\Z/) + click_link('conflicts', href: /\/conflicts\Z/) click_button 'Side-by-side' end diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index 4162f2579d1..ee6985ad993 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -24,6 +24,7 @@ describe "Compare", js: true do expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding") click_button "Compare" + expect(page).to have_content "Commits" end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index c1ecb46aece..8fcf7f5fa15 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -192,4 +192,22 @@ describe IssuablesHelper do expect(helper.issuable_filter_present?).to be_falsey end end + + describe '#updated_at_by' do + let(:user) { create(:user) } + let(:unedited_issuable) { create(:issue) } + let(:edited_issuable) { create(:issue, last_edited_by: user, created_at: 3.days.ago, updated_at: 2.days.ago, last_edited_at: 2.days.ago) } + let(:edited_updated_at_by) do + { + updatedAt: edited_issuable.updated_at.to_time.iso8601, + updatedBy: { + name: user.name, + path: user_path(user) + } + } + end + + it { expect(helper.updated_at_by(unedited_issuable)).to eq({}) } + it { expect(helper.updated_at_by(edited_issuable)).to eq(edited_updated_at_by) } + end end diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index b05ae5c2232..cb727430117 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -52,6 +52,14 @@ describe SubmoduleHelper do stub_url(['http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git'].join('')) expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')]) end + + it 'works with subgroups' do + allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') + allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) + stub_url(['http://', config.host, '/gitlab/root/gitlab-org/sub/gitlab-ce.git'].join('')) + expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org/sub', 'gitlab-ce'), namespace_project_tree_path('gitlab-org/sub', 'gitlab-ce', 'hash')]) + end end context 'submodule on github.com' do diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 0030a953119..59c006aa0af 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -14,6 +14,10 @@ const issueShowInterceptor = data => (request, next) => { })); }; +function formatText(text) { + return text.trim().replace(/\s\s+/g, ' '); +} + describe('Issuable output', () => { document.body.innerHTML = '<span id="task_status"></span>'; @@ -50,12 +54,17 @@ describe('Issuable output', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor); }); - it('should render a title/description and update title/description on update', (done) => { + it('should render a title/description/edited and update title/description/edited on update', (done) => { setTimeout(() => { + const editedText = vm.$el.querySelector('.edited-text'); + expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>'); expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>'); expect(vm.$el.querySelector('.js-task-list-field').value).toContain('this is a description'); + expect(formatText(editedText.innerText)).toMatch(/Edited[\s\S]+?by Some User/); + expect(editedText.querySelector('.author_link').href).toMatch(/\/some_user$/); + expect(editedText.querySelector('time')).toBeTruthy(); Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); @@ -64,6 +73,10 @@ describe('Issuable output', () => { expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>'); expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42'); + expect(vm.$el.querySelector('.edited-text')).toBeTruthy(); + expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(/Edited[\s\S]+?by Other User/); + expect(editedText.querySelector('.author_link').href).toMatch(/\/other_user$/); + expect(editedText.querySelector('time')).toBeTruthy(); done(); }); diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/javascripts/issue_show/components/edited_spec.js new file mode 100644 index 00000000000..a0d0750ae34 --- /dev/null +++ b/spec/javascripts/issue_show/components/edited_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import edited from '~/issue_show/components/edited.vue'; + +function formatText(text) { + return text.trim().replace(/\s\s+/g, ' '); +} + +describe('edited', () => { + const EditedComponent = Vue.extend(edited); + + it('should render an edited at+by string', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedAt: '2017-05-15T12:31:04.428Z', + updatedByName: 'Some User', + updatedByPath: '/some_user', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).toMatch(/Edited[\s\S]+?by Some User/); + expect(editedComponent.$el.querySelector('.author_link').href).toMatch(/\/some_user$/); + expect(editedComponent.$el.querySelector('time')).toBeTruthy(); + }); + + it('if no updatedAt is provided, no time element will be rendered', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedByName: 'Some User', + updatedByPath: '/some_user', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).toMatch(/Edited by Some User/); + expect(editedComponent.$el.querySelector('.author_link').href).toMatch(/\/some_user$/); + expect(editedComponent.$el.querySelector('time')).toBeFalsy(); + }); + + it('if no updatedByName and updatedByPath is provided, no user element will be rendered', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedAt: '2017-05-15T12:31:04.428Z', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).not.toMatch(/by Some User/); + expect(editedComponent.$el.querySelector('.author_link')).toBeFalsy(); + expect(editedComponent.$el.querySelector('time')).toBeTruthy(); + }); +}); diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js index 6683d581bc5..eb3111412a7 100644 --- a/spec/javascripts/issue_show/mock_data.js +++ b/spec/javascripts/issue_show/mock_data.js @@ -5,7 +5,9 @@ export default { description: '<p>this is a description!</p>', description_text: 'this is a description', task_status: '2 of 4 completed', - updated_at: new Date().toString(), + updated_at: '2015-05-15T12:31:04.428Z', + updated_by_name: 'Some User', + updated_by_path: '/some_user', }, secondRequest: { title: '<p>2</p>', @@ -13,7 +15,9 @@ export default { description: '<p>42</p>', description_text: '42', task_status: '0 of 0 completed', - updated_at: new Date().toString(), + updated_at: '2016-05-15T12:31:04.428Z', + updated_by_name: 'Other User', + updated_by_path: '/other_user', }, issueSpecRequest: { title: '<p>this is a title</p>', @@ -21,6 +25,8 @@ export default { description: '<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>', description_text: '- [ ] Task List Item', task_status: '0 of 1 completed', - updated_at: new Date().toString(), + updated_at: '2017-05-15T12:31:04.428Z', + updated_by_name: 'Last User', + updated_by_path: '/last_user', }, }; diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/javascripts/lib/utils/ajax_cache_spec.js index e1747a82329..2c946802dcd 100644 --- a/spec/javascripts/lib/utils/ajax_cache_spec.js +++ b/spec/javascripts/lib/utils/ajax_cache_spec.js @@ -154,5 +154,36 @@ describe('AjaxCache', () => { .then(done) .catch(fail); }); + + it('makes Ajax call even if matching data exists when forceRequest parameter is provided', (done) => { + const oldDummyResponse = { + important: 'old dummy data', + }; + + AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse; + + ajaxSpy = (url) => { + expect(url).toBe(dummyEndpoint); + const deferred = $.Deferred(); + deferred.resolve(dummyResponse); + return deferred.promise(); + }; + + // Call without forceRetrieve param + AjaxCache.retrieve(dummyEndpoint) + .then((data) => { + expect(data).toBe(oldDummyResponse); + }) + .then(done) + .catch(fail); + + // Call with forceRetrieve param + AjaxCache.retrieve(dummyEndpoint, true) + .then((data) => { + expect(data).toBe(dummyResponse); + }) + .then(done) + .catch(fail); + }); }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 17aa70ff3f1..24335614e09 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -13,6 +13,23 @@ import '~/notes'; window.gl = window.gl || {}; gl.utils = gl.utils || {}; + const htmlEscape = (comment) => { + const escapedString = comment.replace(/["&'<>]/g, (a) => { + const escapedToken = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }[a]; + + return escapedToken; + }); + + return escapedString; + }; + describe('Notes', function() { const FLASH_TYPE_ALERT = 'alert'; var commentsTemplate = 'issues/issue_with_comment.html.raw'; @@ -445,11 +462,17 @@ import '~/notes'; }); describe('getFormData', () => { - it('should return form metadata object from form reference', () => { + let $form; + let sampleComment; + + beforeEach(() => { this.notes = new Notes('', []); - const $form = $('form'); - const sampleComment = 'foobar'; + $form = $('form'); + sampleComment = 'foobar'; + }); + + it('should return form metadata object from form reference', () => { $form.find('textarea.js-note-text').val(sampleComment); const { formData, formContent, formAction } = this.notes.getFormData($form); @@ -457,6 +480,18 @@ import '~/notes'; expect(formContent).toEqual(sampleComment); expect(formAction).toEqual($form.attr('action')); }); + + it('should return form metadata with sanitized formContent from form reference', () => { + spyOn(_, 'escape').and.callFake(htmlEscape); + + sampleComment = '<script>alert("Boom!");</script>'; + $form.find('textarea.js-note-text').val(sampleComment); + + const { formContent } = this.notes.getFormData($form); + + expect(_.escape).toHaveBeenCalledWith(sampleComment); + expect(formContent).toEqual('<script>alert("Boom!");</script>'); + }); }); describe('hasSlashCommands', () => { @@ -512,30 +547,42 @@ import '~/notes'; }); }); + describe('getSlashCommandDescription', () => { + const availableSlashCommands = [ + { name: 'close', description: 'Close this issue', params: [] }, + { name: 'title', description: 'Change title', params: [{}] }, + { name: 'estimate', description: 'Set time estimate', params: [{}] } + ]; + + beforeEach(() => { + this.notes = new Notes(); + }); + + it('should return executing slash command description when note has single slash command', () => { + const sampleComment = '/close'; + expect(this.notes.getSlashCommandDescription(sampleComment, availableSlashCommands)).toBe('Applying command to close this issue'); + }); + + it('should return generic multiple slash command description when note has multiple slash commands', () => { + const sampleComment = '/close\n/title [Duplicate] Issue foobar'; + expect(this.notes.getSlashCommandDescription(sampleComment, availableSlashCommands)).toBe('Applying multiple commands'); + }); + + it('should return generic slash command description when available slash commands list is not populated', () => { + const sampleComment = '/close\n/title [Duplicate] Issue foobar'; + expect(this.notes.getSlashCommandDescription(sampleComment)).toBe('Applying command'); + }); + }); + describe('createPlaceholderNote', () => { const sampleComment = 'foobar'; const uniqueId = 'b1234-a4567'; const currentUsername = 'root'; const currentUserFullname = 'Administrator'; + const currentUserAvatar = 'avatar_url'; beforeEach(() => { this.notes = new Notes('', []); - spyOn(_, 'escape').and.callFake((comment) => { - const escapedString = comment.replace(/["&'<>]/g, (a) => { - const escapedToken = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '`': '`' - }[a]; - - return escapedToken; - }); - - return escapedString; - }); }); it('should return constructed placeholder element for regular note based on form contents', () => { @@ -544,46 +591,59 @@ import '~/notes'; uniqueId, isDiscussionNote: false, currentUsername, - currentUserFullname + currentUserFullname, + currentUserAvatar, }); const $tempNoteHeader = $tempNote.find('.note-header'); expect($tempNote.prop('nodeName')).toEqual('LI'); expect($tempNote.attr('id')).toEqual(uniqueId); + expect($tempNote.hasClass('being-posted')).toBeTruthy(); + expect($tempNote.hasClass('fade-in-half')).toBeTruthy(); $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() { expect($(this).attr('href')).toEqual(`/${currentUsername}`); }); + expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar); expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy(); expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname); expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`); expect($tempNote.find('.note-body .note-text p').text().trim()).toEqual(sampleComment); }); - it('should escape HTML characters from note based on form contents', () => { - const commentWithHtml = '<script>alert("Boom!");</script>'; + it('should return constructed placeholder element for discussion note based on form contents', () => { const $tempNote = this.notes.createPlaceholderNote({ - formContent: commentWithHtml, + formContent: sampleComment, uniqueId, - isDiscussionNote: false, + isDiscussionNote: true, currentUsername, currentUserFullname }); - expect(_.escape).toHaveBeenCalledWith(commentWithHtml); - expect($tempNote.find('.note-body .note-text p').html()).toEqual('<script>alert("Boom!");</script>'); + expect($tempNote.prop('nodeName')).toEqual('LI'); + expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); }); + }); - it('should return constructed placeholder element for discussion note based on form contents', () => { - const $tempNote = this.notes.createPlaceholderNote({ - formContent: sampleComment, + describe('createPlaceholderSystemNote', () => { + const sampleCommandDescription = 'Applying command to close this issue'; + const uniqueId = 'b1234-a4567'; + + beforeEach(() => { + this.notes = new Notes('', []); + spyOn(_, 'escape').and.callFake(htmlEscape); + }); + + it('should return constructed placeholder element for system note based on form contents', () => { + const $tempNote = this.notes.createPlaceholderSystemNote({ + formContent: sampleCommandDescription, uniqueId, - isDiscussionNote: true, - currentUsername, - currentUserFullname }); expect($tempNote.prop('nodeName')).toEqual('LI'); - expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); + expect($tempNote.attr('id')).toEqual(uniqueId); + expect($tempNote.hasClass('being-posted')).toBeTruthy(); + expect($tempNote.hasClass('fade-in-half')).toBeTruthy(); + expect($tempNote.find('.timeline-content i').text().trim()).toEqual(sampleCommandDescription); }); }); @@ -595,7 +655,7 @@ import '~/notes'; it('shows a flash message', () => { this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline); - expect(document.querySelectorAll('.flash-alert').length).toBe(1); + expect($('.flash-alert').is(':visible')).toBeTruthy(); }); }); @@ -605,13 +665,12 @@ import '~/notes'; this.notes = new Notes(); }); - it('removes all the associated flash messages', () => { + it('hides visible flash message', () => { this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline); - this.notes.addFlash('Error message 2', FLASH_TYPE_ALERT, this.notes.parentTimeline); this.notes.clearFlash(); - expect(document.querySelectorAll('.flash-alert').length).toBe(0); + expect($('.flash-alert').is(':visible')).toBeFalsy(); }); }); }); diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index 0418fc0a1e2..269798c7c9e 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -67,6 +67,17 @@ describe Gitlab::EtagCaching::Router do expect(result.name).to eq 'merge_request_pipelines' end + it 'matches build endpoint' do + env = build_env( + '/my-group/my-project/builds/234.json' + ) + + result = described_class.match(env) + + expect(result).to be_present + expect(result.name).to eq 'project_build' + end + it 'does not match blob with confusing name' do env = build_env( '/my-group/my-project/blob/master/pipelines.json' diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 3565e719ad3..a9a7bba2c05 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -341,7 +341,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when diff is quite large will collapse by default' do - let(:iterator) { [{ diff: 'a' * 20480 }] } + let(:iterator) { [{ diff: 'a' * (Gitlab::Git::Diff.collapse_limit + 1) }] } + let(:max_files) { 100 } context 'when no collapse is set' do let(:expanded) { true } diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 9c2e8a298c6..da213f617cc 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -31,6 +31,36 @@ EOT [".gitmodules"]).patches.first end + describe 'size limit feature toggles' do + context 'when the feature gitlab_git_diff_size_limit_increase is enabled' do + before do + Feature.enable('gitlab_git_diff_size_limit_increase') + end + + it 'returns 200 KB for size_limit' do + expect(described_class.size_limit).to eq(200.kilobytes) + end + + it 'returns 100 KB for collapse_limit' do + expect(described_class.collapse_limit).to eq(100.kilobytes) + end + end + + context 'when the feature gitlab_git_diff_size_limit_increase is disabled' do + before do + Feature.disable('gitlab_git_diff_size_limit_increase') + end + + it 'returns 100 KB for size_limit' do + expect(described_class.size_limit).to eq(100.kilobytes) + end + + it 'returns 10 KB for collapse_limit' do + expect(described_class.collapse_limit).to eq(10.kilobytes) + end + end + end + describe '.new' do context 'using a Hash' do context 'with a small diff' do @@ -47,7 +77,7 @@ EOT context 'using a diff that is too large' do it 'prunes the diff' do - diff = described_class.new(diff: 'a' * 204800) + diff = described_class.new(diff: 'a' * (described_class.size_limit + 1)) expect(diff.diff).to be_empty expect(diff).to be_too_large @@ -85,8 +115,8 @@ EOT # The patch total size is 200, with lines between 21 and 54. # This is a quick-and-dirty way to test this. Ideally, a new patch is # added to the test repo with a size that falls between the real limits. - stub_const("#{described_class}::SIZE_LIMIT", 150) - stub_const("#{described_class}::COLLAPSE_LIMIT", 100) + allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(150) + allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(100) end it 'prunes the diff as a large diff instead of as a collapsed diff' do @@ -299,7 +329,7 @@ EOT describe '#collapsed?' do it 'returns true for a diff that is quite large' do - diff = described_class.new({ diff: 'a' * 20480 }, expanded: false) + diff = described_class.new({ diff: 'a' * (described_class.collapse_limit + 1) }, expanded: false) expect(diff).to be_collapsed end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index e2406290c6c..b0716e04d3d 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -427,6 +427,42 @@ describe Ci::Build, :models do end end + describe '#environment_url' do + subject { job.environment_url } + + context 'when yaml environment uses $CI_COMMIT_REF_NAME' do + let(:job) do + create(:ci_build, + ref: 'master', + options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } }) + end + + it { is_expected.to eq('http://review/master') } + end + + context 'when yaml environment uses yaml_variables containing symbol keys' do + let(:job) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + options: { environment: { url: 'http://review/$APP_HOST' } }) + end + + it { is_expected.to eq('http://review/host') } + end + + context 'when yaml environment does not have url' do + let(:job) { create(:ci_build, environment: 'staging') } + + let!(:environment) do + create(:environment, project: job.project, name: job.environment) + end + + it 'returns the external_url from persisted environment' do + is_expected.to eq(environment.external_url) + end + end + end + describe '#starts_environment?' do subject { build.starts_environment? } @@ -918,6 +954,10 @@ describe Ci::Build, :models do it { is_expected.to eq(environment) } end + + context 'when there is no environment' do + it { is_expected.to be_nil } + end end describe '#play' do @@ -1139,6 +1179,7 @@ describe Ci::Build, :models do { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: project.path, public: true }, { key: 'CI_PROJECT_PATH', value: project.full_path, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path.parameterize, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, @@ -1176,11 +1217,6 @@ describe Ci::Build, :models do end context 'when build has an environment' do - before do - build.update(environment: 'production') - create(:environment, project: build.project, name: 'production', slug: 'prod-slug') - end - let(:environment_variables) do [ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, @@ -1188,7 +1224,56 @@ describe Ci::Build, :models do ] end - it { environment_variables.each { |v| is_expected.to include(v) } } + let!(:environment) do + create(:environment, + project: build.project, + name: 'production', + slug: 'prod-slug', + external_url: '') + end + + before do + build.update(environment: 'production') + end + + shared_examples 'containing environment variables' do + it { environment_variables.each { |v| is_expected.to include(v) } } + end + + context 'when no URL was set' do + it_behaves_like 'containing environment variables' + + it 'does not have CI_ENVIRONMENT_URL' do + keys = subject.map { |var| var[:key] } + + expect(keys).not_to include('CI_ENVIRONMENT_URL') + end + end + + context 'when an URL was set' do + let(:url) { 'http://host/test' } + + before do + environment_variables << + { key: 'CI_ENVIRONMENT_URL', value: url, public: true } + end + + context 'when the URL was set from the job' do + before do + build.update(options: { environment: { url: 'http://host/$CI_JOB_NAME' } }) + end + + it_behaves_like 'containing environment variables' + end + + context 'when the URL was not set from the job, but environment' do + before do + environment.update(external_url: url) + end + + it_behaves_like 'containing environment variables' + end + end end context 'when build started manually' do diff --git a/spec/models/concerns/editable_spec.rb b/spec/models/concerns/editable_spec.rb new file mode 100644 index 00000000000..cd73af3b480 --- /dev/null +++ b/spec/models/concerns/editable_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Editable do + describe '#is_edited?' do + let(:issue) { create(:issue, last_edited_at: nil) } + let(:edited_issue) { create(:issue, created_at: 3.days.ago, last_edited_at: 2.days.ago) } + + it { expect(issue.is_edited?).to eq(false) } + it { expect(edited_issue.is_edited?).to eq(true) } + end +end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 4c37a553227..a8331ceb5ff 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -9,11 +9,12 @@ describe GroupPolicy, models: true do let(:admin) { create(:admin) } let(:group) { create(:group) } + let(:reporter_permissions) { [:admin_label] } + let(:master_permissions) do [ :create_projects, - :admin_milestones, - :admin_label + :admin_milestones ] end @@ -42,6 +43,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.not_to include(*reporter_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -52,6 +54,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.not_to include(*reporter_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -62,6 +65,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -72,6 +76,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -82,6 +87,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -92,6 +98,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.to include(*master_permissions) is_expected.to include(*owner_permissions) end @@ -102,14 +109,27 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.to include(*master_permissions) is_expected.to include(*owner_permissions) end end - describe 'private nested group inherit permissions', :nested_groups do + describe 'private nested group use the highest access level from the group and inherited permissions', :nested_groups do let(:nested_group) { create(:group, :private, parent: group) } + before do + nested_group.add_guest(guest) + nested_group.add_guest(reporter) + nested_group.add_guest(developer) + nested_group.add_guest(master) + + group.owners.destroy_all + + group.add_guest(owner) + nested_group.add_owner(owner) + end + subject { described_class.abilities(current_user, nested_group).to_set } context 'with no user' do @@ -117,6 +137,7 @@ describe GroupPolicy, models: true do it do is_expected.not_to include(:read_group) + is_expected.not_to include(*reporter_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -127,6 +148,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.not_to include(*reporter_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -137,6 +159,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -147,6 +170,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -157,6 +181,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.to include(*master_permissions) is_expected.not_to include(*owner_permissions) end @@ -167,6 +192,7 @@ describe GroupPolicy, models: true do it do is_expected.to include(:read_group) + is_expected.to include(*reporter_permissions) is_expected.to include(*master_permissions) is_expected.to include(*owner_permissions) end diff --git a/spec/presenters/conversational_development_index/metric_presenter_spec.rb b/spec/presenters/conversational_development_index/metric_presenter_spec.rb new file mode 100644 index 00000000000..1e015c71f5b --- /dev/null +++ b/spec/presenters/conversational_development_index/metric_presenter_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe ConversationalDevelopmentIndex::MetricPresenter do + subject { described_class.new(metric) } + let(:metric) { build(:conversational_development_index_metric) } + + describe '#cards' do + it 'includes instance score, leader score and percentage score' do + issues_card = subject.cards.first + + expect(issues_card.instance_score).to eq 1.234 + expect(issues_card.leader_score).to eq 9.256 + expect(issues_card.percentage_score).to be_within(0.1).of(13.3) + end + end + + describe '#idea_to_production_steps' do + it 'returns percentage score when it depends on a single feature' do + code_step = subject.idea_to_production_steps.fourth + + expect(code_step.percentage_score).to be_within(0.1).of(50.0) + end + + it 'returns percentage score when it depends on two features' do + issue_step = subject.idea_to_production_steps.second + + expect(issue_step.percentage_score).to be_within(0.1).of(53.0) + end + end + + describe '#average_percentage_score' do + it 'calculates an average value across all the features' do + expect(subject.average_percentage_score).to be_within(0.1).of(55.8) + end + end +end diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb index b4eef20d6a6..ad0d3d3839e 100644 --- a/spec/serializers/build_artifact_entity_spec.rb +++ b/spec/serializers/build_artifact_entity_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe BuildArtifactEntity do - let(:job) { create(:ci_build, name: 'test:job') } + let(:job) { create(:ci_build, name: 'test:job', artifacts_expire_at: 1.hour.from_now) } let(:entity) do described_class.new(job, request: double) @@ -14,9 +14,19 @@ describe BuildArtifactEntity do expect(subject[:name]).to eq 'test:job' end - it 'contains path to the artifacts' do + it 'exposes information about expiration of artifacts' do + expect(subject).to include(:expired, :expire_at) + end + + it 'contains paths to the artifacts' do expect(subject[:path]) .to include "jobs/#{job.id}/artifacts/download" + + expect(subject[:keep_path]) + .to include "jobs/#{job.id}/artifacts/keep" + + expect(subject[:browse_path]) + .to include "jobs/#{job.id}/artifacts/browse" end end end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb new file mode 100644 index 00000000000..e2511e8968c --- /dev/null +++ b/spec/serializers/build_details_entity_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe BuildDetailsEntity do + set(:user) { create(:admin) } + + it 'inherits from BuildEntity' do + expect(described_class).to be < BuildEntity + end + + describe '#as_json' do + let(:project) { create(:project, :repository) } + let!(:build) { create(:ci_build, :failed, project: project) } + let(:request) { double('request') } + let(:entity) { described_class.new(build, request: request, current_user: user, project: project) } + subject { entity.as_json } + + before do + allow(request).to receive(:current_user).and_return(user) + end + + context 'when the user has access to issues and merge requests' do + let!(:merge_request) do + create(:merge_request, source_project: project, source_branch: build.ref) + end + + before do + allow(build).to receive(:merge_request).and_return(merge_request) + end + + it 'contains the needed key value pairs' do + expect(subject).to include(:coverage, :erased_at, :duration) + expect(subject).to include(:artifacts, :runner, :pipeline) + expect(subject).to include(:raw_path, :merge_request) + expect(subject).to include(:new_issue_path) + end + + it 'exposes details of the merge request' do + expect(subject[:merge_request]).to include(:iid, :path) + end + + context 'when the build has been erased' do + let!(:build) { create(:ci_build, :erasable, project: project) } + + it 'exposes the user whom erased the build' do + expect(subject).to include(:erase_path) + end + end + + context 'when the build has been erased' do + let!(:build) { create(:ci_build, erased_at: Time.now, project: project, erased_by: user) } + + it 'exposes the user whom erased the build' do + expect(subject).to include(:erased_by) + end + end + end + + context 'when the user can only read the build' do + let(:user) { create(:user) } + + it "won't display the paths to issues and merge requests" do + expect(subject['new_issue_path']).to be_nil + expect(subject['merge_request_path']).to be_nil + end + end + end +end diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index 6d5e1046e86..46d43a80ef7 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe BuildEntity do let(:user) { create(:user) } - let(:build) { create(:ci_build) } + let(:build) { create(:ci_build, :failed) } let(:project) { build.project } let(:request) { double('request') } @@ -18,6 +18,7 @@ describe BuildEntity do it 'contains paths to build page and retry action' do expect(subject).to include(:build_path, :retry_path) + expect(subject[:retry_path]).not_to be_nil end it 'does not contain sensitive information' do diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb index b75c73e78c2..d38433c2365 100644 --- a/spec/serializers/merge_request_entity_spec.rb +++ b/spec/serializers/merge_request_entity_spec.rb @@ -26,7 +26,7 @@ describe MergeRequestEntity do pipeline = build_stubbed(:ci_pipeline) allow(resource).to receive(:head_pipeline).and_return(pipeline) - pipeline_payload = PipelineEntity + pipeline_payload = PipelineDetailsEntity .represent(pipeline, request: req) .as_json diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb new file mode 100644 index 00000000000..03cc5ae9b63 --- /dev/null +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe PipelineDetailsEntity do + set(:user) { create(:user) } + let(:request) { double('request') } + + it 'inherrits from PipelineEntity' do + expect(described_class).to be < PipelineEntity + end + + before do + allow(request).to receive(:current_user).and_return(user) + end + + let(:entity) do + described_class.represent(pipeline, request: request) + end + + describe '#as_json' do + subject { entity.as_json } + + context 'when pipeline is empty' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'contains details' do + expect(subject).to include :details + expect(subject[:details]) + .to include :duration, :finished_at + expect(subject[:details]) + .to include :stages, :artifacts, :manual_actions + expect(subject[:details][:status]).to include :icon, :favicon, :text, :label + end + + it 'contains flags' do + expect(subject).to include :flags + expect(subject[:flags]) + .to include :latest, :stuck, + :yaml_errors, :retryable, :cancelable + end + end + + context 'when pipeline is retryable' do + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_pipeline, status: :success, project: project) + end + + before do + create(:ci_build, :failed, pipeline: pipeline) + end + + context 'user has ability to retry pipeline' do + before { project.team << [user, :developer] } + + it 'retryable flag is true' do + expect(subject[:flags][:retryable]).to eq true + end + end + + context 'user does not have ability to retry pipeline' do + it 'retryable flag is false' do + expect(subject[:flags][:retryable]).to eq false + end + end + end + + context 'when pipeline is cancelable' do + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_pipeline, status: :running, project: project) + end + + before do + create(:ci_build, :pending, pipeline: pipeline) + end + + context 'user has ability to cancel pipeline' do + before { project.add_developer(user) } + + it 'cancelable flag is true' do + expect(subject[:flags][:cancelable]).to eq true + end + end + + context 'user does not have ability to cancel pipeline' do + it 'cancelable flag is false' do + expect(subject[:flags][:cancelable]).to eq false + end + end + end + + context 'when pipeline has YAML errors' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: { invalid: :value } }) + end + + it 'contains information about error' do + expect(subject[:yaml_errors]).to be_present + end + + it 'contains flag that indicates there are errors' do + expect(subject[:flags][:yaml_errors]).to be true + end + end + + context 'when pipeline does not have YAML errors' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'does not contain field that normally holds an error' do + expect(subject).not_to have_key(:yaml_errors) + end + + it 'contains flag that indicates there are no errors' do + expect(subject[:flags][:yaml_errors]).to be false + end + end + end +end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index 88ec4ed2952..a059c2cc736 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe PipelineEntity do - let(:user) { create(:user) } + set(:user) { create(:user) } let(:request) { double('request') } before do @@ -28,8 +28,6 @@ describe PipelineEntity do expect(subject).to include :details expect(subject[:details]) .to include :duration, :finished_at - expect(subject[:details]) - .to include :stages, :artifacts, :manual_actions expect(subject[:details][:status]).to include :icon, :favicon, :text, :label end @@ -55,20 +53,12 @@ describe PipelineEntity do context 'user has ability to retry pipeline' do before { project.team << [user, :developer] } - it 'retryable flag is true' do - expect(subject[:flags][:retryable]).to eq true - end - it 'contains retry path' do expect(subject[:retry_path]).to be_present end end context 'user does not have ability to retry pipeline' do - it 'retryable flag is false' do - expect(subject[:flags][:retryable]).to eq false - end - it 'does not contain retry path' do expect(subject).not_to have_key(:retry_path) end @@ -87,11 +77,7 @@ describe PipelineEntity do end context 'user has ability to cancel pipeline' do - before { project.team << [user, :developer] } - - it 'cancelable flag is true' do - expect(subject[:flags][:cancelable]).to eq true - end + before { project.add_developer(user) } it 'contains cancel path' do expect(subject[:cancel_path]).to be_present @@ -99,42 +85,12 @@ describe PipelineEntity do end context 'user does not have ability to cancel pipeline' do - it 'cancelable flag is false' do - expect(subject[:flags][:cancelable]).to eq false - end - it 'does not contain cancel path' do expect(subject).not_to have_key(:cancel_path) end end end - context 'when pipeline has YAML errors' do - let(:pipeline) do - create(:ci_pipeline, config: { rspec: { invalid: :value } }) - end - - it 'contains flag that indicates there are errors' do - expect(subject[:flags][:yaml_errors]).to be true - end - - it 'contains information about error' do - expect(subject[:yaml_errors]).to be_present - end - end - - context 'when pipeline does not have YAML errors' do - let(:pipeline) { create(:ci_empty_pipeline) } - - it 'contains flag that indicates there are no errors' do - expect(subject[:flags][:yaml_errors]).to be false - end - - it 'does not contain field that normally holds an error' do - expect(subject).not_to have_key(:yaml_errors) - end - end - context 'when pipeline ref is empty' do let(:pipeline) { create(:ci_empty_pipeline) } diff --git a/spec/serializers/runner_entity_spec.rb b/spec/serializers/runner_entity_spec.rb new file mode 100644 index 00000000000..4f25a8dcfa0 --- /dev/null +++ b/spec/serializers/runner_entity_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe RunnerEntity do + let(:runner) { create(:ci_runner, :specific) } + let(:entity) { described_class.new(runner, request: request, current_user: user) } + let(:request) { double('request') } + let(:project) { create(:empty_project) } + let(:user) { create(:admin) } + + before do + allow(request).to receive(:current_user).and_return(user) + allow(request).to receive(:project).and_return(project) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains required fields' do + expect(subject).to include(:id, :description) + expect(subject).to include(:edit_path) + end + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 06fbd7bad90..597c3947e71 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -296,5 +296,20 @@ describe Ci::CreatePipelineService, services: true do expect(Environment.find_by(name: "review/master")).not_to be_nil end end + + context 'when environment with invalid name' do + before do + config = YAML.dump(deploy: { environment: { name: 'name,with,commas' }, script: 'ls' }) + stub_ci_pipeline_yaml_file(config) + end + + it 'does not create an environment' do + expect do + result = execute_service + + expect(result).to be_persisted + end.not_to change { Environment.count } + end + end end end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index f35d7a33548..5398b5c3f7e 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -1,156 +1,117 @@ require 'spec_helper' describe CreateDeploymentService, services: true do - let(:project) { create(:empty_project) } let(:user) { create(:user) } + let(:options) { nil } + + let(:job) do + create(:ci_build, + ref: 'master', + tag: false, + environment: 'production', + options: { environment: options }) + end - let(:service) { described_class.new(project, user, params) } + let(:project) { job.project } - describe '#execute' do - let(:options) { nil } - let(:params) do - { - environment: 'production', - ref: 'master', - tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142', - options: options - } - end + let!(:environment) do + create(:environment, project: project, name: 'production') + end - subject { service.execute } + let(:service) { described_class.new(job) } - context 'when no environments exist' do - it 'does create a new environment' do - expect { subject }.to change { Environment.count }.by(1) - end + describe '#execute' do + subject { service.execute } - it 'does create a deployment' do + context 'when environment exists' do + it 'creates a deployment' do expect(subject).to be_persisted end end - context 'when environment exist' do - let!(:environment) { create(:environment, project: project, name: 'production') } - - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end + context 'when environment does not exist' do + let(:environment) {} - it 'does create a deployment' do - expect(subject).to be_persisted + it 'does not create a deployment' do + expect do + expect(subject).to be_nil + end.not_to change { Deployment.count } end + end - context 'and start action is defined' do - let(:options) { { action: 'start' } } + context 'when start action is defined' do + let(:options) { { action: 'start' } } - context 'and environment is stopped' do - before do - environment.stop - end + context 'and environment is stopped' do + before do + environment.stop + end - it 'makes environment available' do - subject + it 'makes environment available' do + subject - expect(environment.reload).to be_available - end + expect(environment.reload).to be_available + end - it 'does create a deployment' do - expect(subject).to be_persisted - end + it 'creates a deployment' do + expect(subject).to be_persisted end end + end - context 'and stop action is defined' do - let(:options) { { action: 'stop' } } - - context 'and environment is available' do - before do - environment.start - end - - it 'makes environment stopped' do - subject - - expect(environment.reload).to be_stopped - end + context 'when stop action is defined' do + let(:options) { { action: 'stop' } } - it 'does not create a deployment' do - expect(subject).to be_nil - end + context 'and environment is available' do + before do + environment.start end - end - end - context 'for environment with invalid name' do - let(:params) do - { - environment: 'name,with,commas', - ref: 'master', - tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142' - } - end + it 'makes environment stopped' do + subject - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end + expect(environment.reload).to be_stopped + end - it 'does not create a deployment' do - expect(subject).to be_nil + it 'does not create a deployment' do + expect(subject).to be_nil + end end end context 'when variables are used' do - let(:params) do - { - environment: 'review-apps/$CI_COMMIT_REF_NAME', - ref: 'master', - tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142', - options: { - name: 'review-apps/$CI_COMMIT_REF_NAME', - url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' - }, - variables: [ - { key: 'CI_COMMIT_REF_NAME', value: 'feature-review-apps' } - ] - } + let(:options) do + { name: 'review-apps/$CI_COMMIT_REF_NAME', + url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' } end - it 'does create a new environment' do - expect { subject }.to change { Environment.count }.by(1) - - expect(subject.environment.name).to eq('review-apps/feature-review-apps') - expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com') + before do + environment.update(name: 'review-apps/master') + job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME') end - it 'does create a new deployment' do + it 'creates a new deployment' do expect(subject).to be_persisted end - context 'and environment exist' do - let!(:environment) { create(:environment, project: project, name: 'review-apps/feature-review-apps') } - - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end - - it 'updates external url' do - subject + it 'does not create a new environment' do + expect { subject }.not_to change { Environment.count } + end - expect(subject.environment.name).to eq('review-apps/feature-review-apps') - expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com') - end + it 'updates external url' do + subject - it 'does create a new deployment' do - expect(subject).to be_persisted - end + expect(subject.environment.name).to eq('review-apps/master') + expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') end end context 'when project was removed' do - let(:project) { nil } + let(:environment) {} + + before do + job.update(project: nil) + end it 'does not create deployment or environment' do expect { subject }.not_to raise_error @@ -162,34 +123,26 @@ describe CreateDeploymentService, services: true do end describe 'processing of builds' do - let(:environment) { nil } - - shared_examples 'does not create environment and deployment' do - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end - + shared_examples 'does not create deployment' do it 'does not create a new deployment' do expect { subject }.not_to change { Deployment.count } end it 'does not call a service' do expect_any_instance_of(described_class).not_to receive(:execute) + subject end end - shared_examples 'does create environment and deployment' do - it 'does create a new environment' do - expect { subject }.to change { Environment.count }.by(1) - end - - it 'does create a new deployment' do + shared_examples 'creates deployment' do + it 'creates a new deployment' do expect { subject }.to change { Deployment.count }.by(1) end - it 'does call a service' do + it 'calls a service' do expect_any_instance_of(described_class).to receive(:execute) + subject end @@ -199,7 +152,7 @@ describe CreateDeploymentService, services: true do expect(Deployment.last.deployable).to eq(deployable) end - it 'create environment has URL set' do + it 'updates environment URL' do subject expect(Deployment.last.environment.external_url).not_to be_nil @@ -207,41 +160,39 @@ describe CreateDeploymentService, services: true do end context 'without environment specified' do - let(:build) { create(:ci_build, project: project) } + let(:job) { create(:ci_build) } - it_behaves_like 'does not create environment and deployment' do - subject { build.success } + it_behaves_like 'does not create deployment' do + subject { job.success } end end context 'when environment is specified' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) } + let(:deployable) { job } + let(:options) do { environment: { name: 'production', url: 'http://gitlab.com' } } end - context 'when build succeeds' do - it_behaves_like 'does create environment and deployment' do - let(:deployable) { build } - - subject { build.success } + context 'when job succeeds' do + it_behaves_like 'creates deployment' do + subject { job.success } end end - context 'when build fails' do - it_behaves_like 'does not create environment and deployment' do - subject { build.drop } + context 'when job fails' do + it_behaves_like 'does not create deployment' do + subject { job.drop } end end - context 'when build is retried' do - it_behaves_like 'does create environment and deployment' do + context 'when job is retried' do + it_behaves_like 'creates deployment' do before do project.add_developer(user) end - let(:deployable) { Ci::Build.retry(build, user) } + let(:deployable) { Ci::Build.retry(job, user) } subject { deployable.success } end @@ -250,15 +201,6 @@ describe CreateDeploymentService, services: true do end describe "merge request metrics" do - let(:params) do - { - environment: 'production', - ref: 'master', - tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142b' - } - end - let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } context "while updating the 'first_deployed_to_production_at' time" do @@ -273,8 +215,8 @@ describe CreateDeploymentService, services: true do end it "doesn't set the time if the deploy's environment is not 'production'" do - staging_params = params.merge(environment: 'staging') - service = described_class.new(project, user, staging_params) + job.update(environment: 'staging') + service = described_class.new(job) service.execute expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil @@ -298,7 +240,7 @@ describe CreateDeploymentService, services: true do expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) # Current deploy - service = described_class.new(project, user, params) + service = described_class.new(job) Timecop.freeze(time + 12.hours) { service.execute } expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) @@ -318,7 +260,7 @@ describe CreateDeploymentService, services: true do expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil # Current deploy - service = described_class.new(project, user, params) + service = described_class.new(job) Timecop.freeze(time + 12.hours) { service.execute } expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb new file mode 100644 index 00000000000..63a1e78f274 --- /dev/null +++ b/spec/services/submit_usage_ping_service_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe SubmitUsagePingService do + context 'when usage ping is disabled' do + before do + stub_application_setting(usage_ping_enabled: false) + end + + it 'does not run' do + expect(HTTParty).not_to receive(:post) + + result = subject.execute + + expect(result).to eq false + end + end + + context 'when usage ping is enabled' do + before do + stub_application_setting(usage_ping_enabled: true) + end + + it 'sends a POST request' do + response = stub_response(without_conv_index_params) + + subject.execute + + expect(response).to have_been_requested + end + + it 'refreshes usage data statistics before submitting' do + stub_response(without_conv_index_params) + + expect(Gitlab::UsageData).to receive(:to_json) + .with(force_refresh: true) + .and_call_original + + subject.execute + end + + it 'saves conversational development index data from the response' do + stub_response(with_conv_index_params) + + expect { subject.execute } + .to change { ConversationalDevelopmentIndex::Metric.count } + .by(1) + + expect(ConversationalDevelopmentIndex::Metric.last.leader_issues).to eq 10.2 + end + end + + def without_conv_index_params + { + conv_index: {} + } + end + + def with_conv_index_params + { + conv_index: { + leader_issues: 10.2, + instance_issues: 3.2, + + leader_notes: 25.3, + instance_notes: 23.2, + + leader_milestones: 16.2, + instance_milestones: 5.5, + + leader_boards: 5.2, + instance_boards: 3.2, + + leader_merge_requests: 5.2, + instance_merge_requests: 3.2, + + leader_ci_pipelines: 25.1, + instance_ci_pipelines: 21.3, + + leader_environments: 3.3, + instance_environments: 2.2, + + leader_deployments: 41.3, + instance_deployments: 15.2, + + leader_projects_prometheus_active: 0.31, + instance_projects_prometheus_active: 0.30, + + leader_service_desk_issues: 15.8, + instance_service_desk_issues: 15.1 + } + } + end + + def stub_response(body) + stub_request(:post, 'https://version.gitlab.com/usage_data'). + to_return( + headers: { 'Content-Type' => 'application/json' }, + body: body.to_json + ) + end +end diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 66545127a44..6e1eb5c678d 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -51,12 +51,43 @@ module CycleAnalyticsHelpers end def deploy_master(environment: 'production') - CreateDeploymentService.new(project, user, { - environment: environment, - ref: 'master', - tag: false, - sha: project.repository.commit('master').sha - }).execute + dummy_job = + case environment + when 'production' + dummy_production_job + when 'staging' + dummy_staging_job + else + raise ArgumentError + end + + CreateDeploymentService.new(dummy_job).execute + end + + def dummy_production_job + @dummy_job ||= new_dummy_job('production') + end + + def dummy_staging_job + @dummy_job ||= new_dummy_job('staging') + end + + def dummy_pipeline + @dummy_pipeline ||= + Ci::Pipeline.new(sha: project.repository.commit('master').sha) + end + + def new_dummy_job(environment) + project.environments.find_or_create_by(name: environment) + + Ci::Build.new( + project: project, + user: user, + environment: environment, + ref: 'master', + tag: false, + name: 'dummy', + pipeline: dummy_pipeline) end end diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb new file mode 100644 index 00000000000..c3b72e7d677 --- /dev/null +++ b/spec/uploaders/lfs_object_uploader_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe LfsObjectUploader do + let(:uploader) { described_class.new(build_stubbed(:empty_project)) } + + describe '#cache!' do + it 'caches the file in the cache directory' do + # One to get the work dir, the other to remove it + expect(uploader).to receive(:workfile_path).exactly(2).times.and_call_original + expect(FileUtils).to receive(:mv).with(anything, /^#{uploader.work_dir}/).and_call_original + expect(FileUtils).to receive(:mv).with(/^#{uploader.work_dir}/, /^#{uploader.cache_dir}/).and_call_original + + fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') + uploader.cache!(fixture_file_upload(fixture)) + + expect(uploader.file.path).to start_with(uploader.cache_dir) + end + end + + describe '#move_to_cache' do + it 'is true' do + expect(uploader.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(uploader.move_to_store).to eq(true) + end + end +end diff --git a/spec/workers/gitlab_usage_ping_worker_spec.rb b/spec/workers/gitlab_usage_ping_worker_spec.rb index 26241044533..49b4e04dc7c 100644 --- a/spec/workers/gitlab_usage_ping_worker_spec.rb +++ b/spec/workers/gitlab_usage_ping_worker_spec.rb @@ -3,21 +3,11 @@ require 'spec_helper' describe GitlabUsagePingWorker do subject { described_class.new } - it "sends POST request" do - stub_application_setting(usage_ping_enabled: true) + it 'delegates to SubmitUsagePingService' do + allow(subject).to receive(:try_obtain_lease).and_return(true) - stub_request(:post, "https://version.gitlab.com/usage_data"). - to_return(status: 200, body: '', headers: {}) - expect(Gitlab::UsageData).to receive(:to_json).with({ force_refresh: true }).and_call_original - expect(subject).to receive(:try_obtain_lease).and_return(true) + expect_any_instance_of(SubmitUsagePingService).to receive(:execute) - expect(subject.perform.response.code.to_i).to eq(200) - end - - it "does not run if usage ping is disabled" do - stub_application_setting(usage_ping_enabled: false) - - expect(subject).not_to receive(:try_obtain_lease) - expect(subject).not_to receive(:perform) + subject.perform end end |